I'm facing a strange issue when adding a property to an intersection type, a generic parameter is loss. Considering the following :
type Service = Record<string, any>;
type Dependencies = Record<string, Service>;
type Parameters = Record<string, any>;
type HandlerFunction<D, P, R> = (d: D, p: P) => R;
type ServiceInitializer<D extends Dependencies, S = any> = {
type: string; // ----<HERE>----
} & ((dependencies?: D) => Promise<S>);
function handler<D extends Dependencies, P extends Parameters, R>(
handlerFunction: HandlerFunction<D, P, R>,
name?: string,
dependencies?: string[],
): ServiceInitializer<D, Promise<(p: P) => R>>;
const x = handler(<T>(d: any, { a }: { a: T }): T => a);
If I remove the line marked with the ----<HERE>----
flag, I end up with the following type for the x
const which preserve the generic type :
const x: <T>(dependencies?: any) => Promise<Promise<(p: {
a: T;
}) => T>>
But if I leave the line, I loose it, T
is replaced by unkown
:
const x: ServiceInitializer<Record<string, Record<string, any>>, Promise<(p: Record<string, any>) => unknown>>
Is there a way to have generic types preserved by still allowing to mix functions and objects properties?
This is just a limitation of the support for higher order type inference from generic functions introduced in TypeScript 3.4, as implemented by microsoft/TypeScript#30215. Before TypeScript 3.4, the compiler would always lose generic function types when inferring their argument or return types, by specifying the type parameters with their constraints. So a function of type <T>(x: T)=>T
would "collapse" to (x: unknown)=>unknown
, resulting in a parameter tuple of [unknown]
and a return type of unknown
.
TypeScript 3.4 added support so that it is sometimes possible to preserve the generic type parameter, but this only happens in very particular circumstances:
When an argument expression in a function call is of a generic function type, the type parameters of that function type are propagated onto the result type of the call if:
- the called function is a generic function that returns a function type with a single call signature,
- that single call signature doesn't itself introduce type parameters, and
- in the left-to-right processing of the function call arguments, no inferences have been made for any of the type parameters referenced in the contextual type for the argument expression.
In your case, you are apparently running afoul of the first (bolded) bullet point. The type parameter is only preserved if the called function (handler()
) has a return type which itself a function type with a "single call signature".
If you inspect the code (somewhere around line 20041 of this file, which is too big to link to directly in GitHub), you'll see that what is considered a "single call signature" is quite restrictive:
// If type has a single call signature and no other members, return that signature.
// Otherwise, return undefined.
function getSingleCallSignature(type: Type): Signature | undefined { ... }
See that? "no other members". Since the return type of handler()
is ServiceInitializer<D, Promise<(p: P) => R>>
, that only counts as a "single call signature" in the version where you don't add the type
member to it. As soon as you add that, it fails to be a "single call signature", and you get the pre-3.4 behavior.
Just to make it clear for people not using your specific code, a minimum example looks something like this:
declare function f<A extends any[], R>(f: (...a: A) => R): ((...a: A) => R);
const g = f(<T>(x: T) => x) // const g: <T>(x: T) => T
declare function h<A extends any[], R>(f: (...a: A) => R): ((...a: A) => R) & { x: 0 };
const i = h(<T>(x: T) => x) // const i: ((x: any) => any) & { x: 0; }
The function f()
returns a "single call signature", so any generic functions passed to it stay generic upon return. But the function h()
returns a function with an extra member, so any generic functions passed to it become non-generic upon return.
The support for higher order type inference is fairly brittle, so this is currently not possible.