I've defined a generics Function, and I'm trying to pass the union of two specific sample of the generics to the function, like
function myCommonFunc<T>({
data,
render,
}: {
data: T;
render: (data: T) => number;
}) {
return render(data);
}
interface TestX<T> {
data: T;
render: (data: T) => number;
}
let z: TestX<number> | TestX<string>;
// let z: TestX<number | string> is wrong, because T will be number | string
if ((window as any).someUserInput === 1) {
z = { data: 1, render: (a: number) => 1 };
} else {
z = { data: '1', render: (a: string) => 1 };
}
// someother function
myCommonFunc(z);
and it throw errs.
I guess the reason is the function cannot infer type from the unionType, and If so, what should I do?
I know one solution is to use typeguard of z, like
function isTypeA(z: TestX<number> | TestX<string>): z is TestX<number> {
return typeof z.data === 'number';
}
if (isTypeA(z)) {
myCommonFunc(z);
}
but I think it actually change the structure of the program, and it need lots if-else to just for typescript, which is annoying. Can someone give me some other way to solve it?
==========
update:
François's solution is helpful, but somehow it cannot solve the real situation, actually z is a state in React Function Component, and myCommonFunc is a Component, and I'll update the real code:
interface TestX<T> {
data: T;
render: (data: T) => number;
}
function MyCommonFunc<T>({ data, render }: TestX<T>) {
return <>{render(data)}</>;
}
type myType = TestX<number> | TestX<string>;
const F = () => {
const [z, setZ] = useState<myType>({ data: 1, render: (a: number) => 1 });
const [userInput, setUserInput] = useState<number>(1);
useEffect(() => {
if (userInput === 1) {
setZ({ data: 1, render: (a: number) => 1 });
} else {
setZ({ data: '1', render: (a: string) => 1 });
}
}, [userInput]);
// someOther Code
return <MyCommonFunc {...z} />;
};
So I cannot just move the function call to the if-else block. and I'm so sorry that not put the real code at the first time.
This is a hard one. I see 3 solutions:
Solution 1 is the one you've mentioned above of using type guards:
const isTestNumber = (z: TestX<number> | TestX<string>): z is TestX<number> => {
return typeof z.data === "number"
}
if (isTestNumber(z)) {
myCommonFunc(z);
} else {
myCommonFunc(z);
}
It is by far my least favorite solution.
Solution 2 is to cast z before passing it as a parameter. It looks like this:
myCommonFunc(z as TestX<any>); // ugly casting
I know it's ugly, but it's not particularly dangerous since the z variable is fully typed above. At least it does not alter the execution of the program.
Solution 3 requires some structural change to your program.
What you currently do is you declare the z variable, you assign it in an if-else statement and then you perform operations on it afterward.
What you could do is wrap all subsequent operations in another generic function.
It looks like this:
interface TestX<T> {
data: T;
render: (data: T) => number;
}
function myCommonFunc<T>(z: TestX<T>) {
const { data, render } = z
return render(data);
}
function allSubsequentOperations<T>(z: TestX<T>) {
// someother function
myCommonFunc(z);
}
if ((window as any).someUserInput === 1) {
const z: TestX<number> = { data: 1, render: (a: number) => 1 };
allSubsequentOperations(z)
} else {
const z: TestX<string> = { data: "1", render: (a: string) => 1 };
allSubsequentOperations(z)
}
This is what I usually do when dealing with this kind of problem. From the moment z is assigned, your whole program becomes generic.
I hope you find these answers helpful,
François