Issue
I am trying to create a function that lets a caller remap an object given a specific mapping. The resulting object should be able to know the new new field names and types.
Is this possible in typescript? I think all I need at this point is a return type. This is non-working code I have:
const mapping = {
a: "learn",
b: "state"
}
const original = {
a: false,
b: 0
}
const map = <
Mapping,
Original
>(
mapping: Mapping,
states: Original
): {
[key: Mapping[key]]: Original[key] //WHAT IS THE CORRECT TYPE HERE?
} => {
return Object.keys(original).reduce((total, key) => {
return {
...total,
[mapping[key]]: original[key] //THERE IS AN ERROR HERE TOO BUT I AM NOT WORRIED ABOUT THAT RIGHT NOW
}
}, {})
}
const remapped = map(mapping, original)
console.log(remapped)
console.log(remapped.learn)
console.log(remapped.state)
I am essentially trying to rename a to learn and b to state. The code is working functionally but I am getting a type error ('key' refers to a value, but is being used as a type here.). Any help would be greatly appreciated!
Solution
First you'll need @jcalz's amazing UnionToIntersection utility type in your toolbox.
type UnionToIntersection<U> =
(U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never;
Now let's make our own utility type:
1 type Remap<A extends { [k: string]: string }, B> =
2 keyof B extends keyof A ?
3 { [P in keyof B & keyof A]: { [k in A[P]]: B[P] } } extends
4 { [s: string]: infer V } ?
5 UnionToIntersection<V>
6 : never
7 : never;
Explain:
- ln:1, constrain A's value to be string.
- ln:2, constrain that all B's keys present in A
- ln:3, map to
{ "a": {"learn": boolean}, "b": {"state": number} } - ln:4, take the value part, which becomes:
{"learn": boolean} | {"state": number}union - ln:5, apply the
UnionToIntersectionmagic!
Put things together:
const mapping = {
a: "learn",
b: "state",
} as const; // mark as `const` is necessary to infer value as string literal
// else it'll just be string
const original = {
a: false,
b: 0,
};
const map = <
M extends { [k: string]: string },
O extends { [k: string]: any }
>(mapping: M, original: O): Remap<M, O> => {
return Object.keys(original).reduce((total, key) => {
return {
...total,
[mapping[key]]: original[key]
}
}, {} as any); // as any to mute error
};
I came across another answer by jcalz, which provides a more elegant solution than mine. Check that answer for explanation. I'll append it here:
type Remap2<
M extends { [k: string]: string },
O extends { [P in keyof M]: any }
> = {
[P in M[keyof M]]: O[
{ [K in keyof M]: M[K] extends P ? K : never }[keyof M]
]
};
Answered By - hackape
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.