Issue
I have an object mapping functions, and an interface (with the same keys as the object) mapping the argument types for said functions. Now I'm trying to write a function that can call either one, depending on a key. Here's a simplified version of the problem:
interface OneFProps {
a: string;
b: string;
}
const oneF = (props: OneFProps) => {
const { a, b } = props;
return `oneF ${a} ${b}`;
};
interface TwoFProps {
c: string;
d: string;
}
const twoF = (props: TwoFProps) => {
const { c, d } = props;
return `oneF ${c} ${d}`;
};
const funcMap = {
oneF: oneF,
twoF: twoF,
};
interface typeMap {
oneF: OneFProps;
twoF: TwoFProps;
}
type tags = keyof typeMap;
type BlendedProps<T extends tags> = {
key: T;
z?: string;
x?: string;
} & typeMap[T];
const blended = <T extends tags>(props: BlendedProps<T>) => {
const { key, z, x, ...rest } = props;
const func = funcMap[key];
return func(rest as any);
};
console.log(blended<'oneF'>({ key: 'oneF', a: 'AAA', b: 'bbb' }));
console.log(blended<'twoF'>({ key: 'twoF', c: 'ccc', d: 'DDD' }));
I had to put that rest as any because I couldn't get the types to work. I did some research and found stuff about distributive conditional types, but I don't know if that's really the problem here and couldn't come up with a solution for that. Is there a way to preserve type safety here or is my approach conceptually wrong?
Solution
The underlying issue here has to do with the compiler being unable to see the correlation between the type of func and the type of rest inside the implementation of blended(). This situation is essentially the subject of microsoft/TypeScript#30581, and the recommended approach to deal with it is detailed in microsoft/TypeScript#47109.
If you want the compiler to verify that funcMap[key] accepts a parameter of type rest, then you need to express the type of funcMap explicitly as a mapped type over the Tags union, where each property takes a single argument of the type the compiler infers for rest. If you look inside blended, the type of rest is Omit<BlendedProps<T>, "key" | "z" | "x">. So you can annotate funcMap like this:
const funcMap: { [T in Tags]:
(props: Omit<BlendedProps<T>, "key" | "z" | "x">) => string
} = {
oneF: oneF,
twoF: twoF,
};
This doesn't really change the type of funcMap; indeed the compiler can see that the initializer value of type { oneF: (props: OneFProps) => string; twoF: (props: TwoFProps) => string; } is assignable to the variable of type { [T in Tags]: (props: Omit<BlendedProps<T>, "key" | "z" | "x">) => string }, so they are compatible types.
But the difference in representation is important, because now the implementation of blended() type checks:
const blended = <T extends Tags>(props: BlendedProps<T>) => {
const { key, z, x, ...rest } = props;
const func = funcMap[key];
return func(rest); // okay
};
Indeed, now the type of funcMap[key] is inferred to be (props: Omit<BlendedProps<T>, "key" | "z" | "x">) => string, and that parameter type is identical to the type of rest, so funcMap[key](rest) is accepted, as desired.
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.