I am trying to make a simple chart function.
However, there is a problem with TypeScript's generic inference. I get an error depending on whether the toItems
function takes a parameter or not. Even it has nothing to do with the value it actually returns. how can I solve this?
Below is the full source code to check the problem.
interface ChartProps<T, U> {
datas: T[];
toItems: (data: T) => U[];
toValue: (items: U) => any;
}
const chart = <T, U>(props: ChartProps<T, U>) => {
return;
};
const datas = [
{ a: { b: [{ c: 1 }] } },
{ a: { b: [{ c: 2 }] } },
{ a: { b: [{ c: 3 }] } }
];
chart({
datas,
toItems: () => [{ c: 1 }],
toValue: ({ c }) => c, // U === {c:number}
});
chart({
datas,
toItems: (a) => [{ c: 1 }],
toValue: ({ c }) => c, // error! U === unknown
});
CodePudding user response:
TypeScript 4.6 and below:
This is a design limitation in TypeScript up to and including version 4.6. See microsoft/TypeScript#38872. When a function has an untyped parameter, the function is considered to be context sensitive because its parameter needs to be contextually typed. The compiler defers evaluating the type of a context sensitive function, as, generally speaking, the return type will depend on its parameter types. Situations where the input parameter has "nothing to do with the value it actually returns" are fairly rare.
So the compiler decides to delay inferring the type of (a) => [{ c: 1 }]
until later. That deferral would be fine, but unfortunately, the inference process up to and including TypeScript 4.6 could only perform one phase of inference per function parameter; since props
is a single function parameter, the compiler would never have a chance to get the type of toItems
right after it tried to infer the generic type parameters, and thus things fail.
The workarounds here were to either manually annotate your callback parameters,
type Data = typeof datas[number];
chart({
datas,
toItems: (a: Data) => [{ c: 1 }],
toValue: ({ c }) => c, // okay
});
or to split the chart()
props
parameter object into three separate parameters:
const chartSplit = <T, U>(
datas: ChartProps<T, U>["datas"],
toItems: ChartProps<T, U>["toItems"],
toValue: ChartProps<T, U>["toValue"]
) => {
return;
};
chartSplit(
datas,
(a) => [{ c: 1 }],
({ c }) => c, // okay
);
Neither of these are great.
Playground link to TS4.6- code
TypeScript 4.7 and above:
Luckily for you, TypeScript 4.7 will introduce improved function inference in objects and methods, as implemented in microsoft/TypeScript#48538.
This will allow the compiler to essentially perform one phase of inference per context-sensitive method in the single props
object. And your original code will just work as-is:
chart({
datas,
toItems: (a) => [{ c: 1 }],
toValue: ({ c }) => c, // okay
});
Note that such inference is order dependent; in an object literal like {key1: val1, key2: val2}
, the compiler can use information from key1
to infer things bout key2
, but not generally vice versa. This is how things always worked for different function parameters (e.g., in f(arg1, arg2)
, the compiler can use information from arg1
to infer things about arg2
, but not generally vice versa) but is kind of new for object literals.
So rearranging your input to chart
which, conceptually, shouldn't matter, will cause things to fail again:
chart({
datas,
toValue: ({ c }) => c, // error!
toItems: (a) => [{ c: 1 }],
});