I have a config that has columns (a tuple) and a callback.
export type FooConfig<
T extends { [N in keyof T]: VerifyFoo<T[N]> },
> = {
columns: T & readonly Foo<any, any, any>[]
transformRows?: (
columns: T,
) => any
};
The columns tuple has objects of the following shape:
export interface Foo<
TData extends GenericData,
TVariables extends GenericVariables,
TFilter,
> {
query: DataType<TData, TVariables>
filterFieldName?: TFilter
}
All of this is put together in the following function:
function create<T extends { [N in keyof T]: VerifyFoo<T[N]> },>(t: FooConfig<T>) {
return t;
}
// inferred generic type is {}
create({
transformRows: (columns) => {
} ,
columns: [
{
query: "" as unknown as DataType<queryOne, queryOneVariables>,
},
{
query: "" as unknown as DataType<queryTwo, queryTwoVariables>,
filterFieldName: 'idA',
},
]} as const
);
The issue is that if the transformRows is not defined in the config the create function works correctly it throws an error if filterFieldName
is incorrect and the generics contains all the data about columns. However when I add the transformRows
callback it breaks and infers {}
as the type of the generic for the create function.
What has me confused is that intellisense seems to work:
Furthermore if I manually type transformRows
, like this:
transformRows: (columns: string) => {},
it gives a type error that the string type cannot be assigned to the generic and print the exact type I want (the error message is very long you can try it yourself on TS playground). I've tried using the NoInfer
from ts-toolbelt
but was unsuccessful.
How could I do this to get all the data about the columns, check filterFieldName
field, and infer the type of transformRows
?
TypeScript's algorithm for inferring generic type arguments and for contextually inferring unannotated function parameters isn't perfect by any means. Without a full unification algorithm as described in microsoft/TypeScript#30134, there will always be situations where inference fails despite there existing a "correct" set of types that should have been inferred. The situation has been steadily improving (e.g., TS4.7 improved function inference in objects and methods), and there are tricks and tips you can use to try to lead inference in a desired direction, but sometimes it just won't work.
This may be one of those times (at least I wasn't able to find something better).
In situations like this, it is sometimes preferable to work around the problem and refactor your code so that inference succeeds. One way to do this is via a fluent builder pattern, where instead of calling the function all at once, you do it in chunks and in an order where each chunk relies only on successful inference from previous chunks.
For example, we can break create()
into pieces, to become a function which first accepts only columns
, and then returns another function that accepts transformRows
. The columns
input can be used to infer the generic type argument T
(and if not, you could imagine breaking this up into a series of calls where each one accepts a key and a value)... and so by the time you call the second function the compiler definitely knows what type the callback parameter to transformRows
will be. Like this:
const create = <T extends { [N in keyof T]: VerifyFoo<T[N]> },>(
columns: T & readonly Foo<any, any, any>[]) =>
(transformRows?: (columns: T) => any) => ({ columns, transformRows });
This ends up creating the same object as before. Let's test it:
const x = create([
{
query: "" as unknown as DataType<queryOne, queryOneVariables>,
},
{
query: "" as unknown as DataType<queryTwo, queryTwoVariables>,
filterFieldName: 'idA',
},
] as const)((columns) => {
/* (parameter) columns: readonly [{
readonly query: DataType<queryOne, queryOneVariables>;
}, {
readonly query: DataType<queryTwo, queryTwoVariables>;
readonly filterFieldName: "idA";
}] */
});
/* const x: {
columns: readonly [{
readonly query: DataType<queryOne, queryOneVariables>;
}, {
readonly query: DataType<queryTwo, queryTwoVariables>;
readonly filterFieldName: "idA";
}];
transformRows: ((columns: readonly [{
readonly query: DataType<queryOne, queryOneVariables>;
}, {
readonly query: DataType<queryTwo, queryTwoVariables>;
readonly filterFieldName: "idA";
}]) => any) | undefined;
} */
Looks good! Inference works as desired.