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?
CodePudding user response:
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.
CodePudding user response:
Hope it helps you
class Props<K extends string, P extends {}> {
key: K;
data: P;
constructor(data: { key: K } & P) {
this.key = data.key;
this.data = data;
}
toString() {
return JSON.stringify(this.data);
}
}
const oneF = new Props({ key: 'oneF', a: 'AAA', b: 'bbb' });
console.log(oneF.toString());
// {"key":"oneF","a":"AAA","b":"bbb"}
const twoF = new Props({ key: 'twoF', c: 'ccc', d: 'DDD' });
console.log(twoF.toString());
// {"key":"twoF","c":"ccc","d":"DDD"}
CodePudding user response:
const blended2 = <T extends tags>(props: BlendedProps<T>) => {
const { key, z, x, ...rest } = props
const func = funcMap[key]
func()
// const func: (props: OneFProps & TwoFProps) => string
// why the props is an intersection type ?
// since function params are contravariance (you can search this to find more ) ,
// in type system , ts can't know what props type is in real runtime
// to make func type safe , only the type of params is the intersection of OneFProps & TwoFProps.
return func(rest as any)
}
how to fix this ?
you need to judge what type props exactly is,but it is too complex,and it also has runtime effect.
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 judgeProps = <U extends tags>(
props: { key: unknown },
target: U
): props is BlendedProps<U> => {
return props.key === target
}
const blended = <T extends tags>(props: BlendedProps<T>) => {
if (judgeProps(props, 'oneF')) {
const { key, z, x, ...rest } = props
const func = funcMap[key]
return func(rest)
} else if (judgeProps(props, 'twoF')) {
const { key, z, x, ...rest } = props
const func = funcMap[key]
return func(rest)
}
}
console.log(blended({ key: 'oneF', a: 'AAA', b: 'bbb' }))
console.log(blended({ key: 'twoF', c: 'ccc', d: 'DDD' }))