Is there a way to hint TS which occurrence of generic parameter to use for generic inference?

Viewed 142

Is there a way to hint TS which occurrence of generic parameter to use for generic inference?

type Handler = <T>( // <-- if T is unspecified in method definition and call how can i tell ts to infer it to Ta from Params<Ta> and not Tb of the input?
  func: (params: Params<Ta>) => any,
  input: Tb
) => string;

code sample

type Params<T = Record<string, unknown>> = {
  p: T;
  // other metadata
}

type Handler = <T>( // <-- if T is unspecified how can i tell ts to infer it from Params<T>?
  func: (params: Params<T>) => any,
  input: T
) => string;

type Config = {
  handler: Handler
}

const c: Config = {
  handler: (params, input) => 'ok' // <-- no possibility to pass/state the generic parameter here
}


type AB = {a:number, b:number}
const myHandler = (params: Params<AB>) => params.p.a + params.p.b;

c.handler(myHandler, { a:1, b:2, c:"unexpected" }) // <-- T is inferred to { a:1, b:2, c:"unexpected" }
c.handler<AB>(myHandler, { a:1, b:2, c:"unexpected" }) // <-- this is desired behaviour, can it be done without stating the T = AB explicitly?

ts playground link

2 Answers

You're looking for the feature requested at microsoft/TypeScript#14829, "Noninferential type parameter usage":

Often with generics, there will be some locations where a type parameter should be inferrable from usage, and other places where the type parameter should only be used to enforce typechecking. This comes up in a variety of contexts.

The proposal there is to come up with some NoInfer<T> syntax, which is equivalent to T except that the compiler would not infer T from that location. This is the flip side of what you're asking for, but they both would achieve the same goal: a NoInfer<T> on the input property type is like a "PleaseInfer<T>" on the input to the func() method.


While there is no official version of NoInfer<T>, there are a few implementations mentioned in the GitHub issue that work for some use cases. One I sometimes recommend is:

type NoInfer<T> = [T][T extends any ? 0 : never];

This works by taking advantage of the compilers deferral of evaluating distributive conditional types when the checked type is an unresolved generic. It cannot "see" that NoInfer<T> will evaluate to T, until T is some specific resolved type, such as after T has been inferred.


Armed with that, if I change your example code to:

type Handler = <T>(
  func: (params: Params<T>) => any,
  input: NoInfer<T>
) => string;

Then your examples work as expected:

const c: Config = {
  handler: (func, input) => func({ p: input }) // okay,
}
   
type AB = { a: number, b: number }
const myHandler = (params: Params<AB>) => params.p.a + params.p.b;

c.handler(myHandler, { a: 1, b: 2, c: "unexpected" }); // error!
// ------------------------------> ~~~~~~~~~~~~~~~
// Object literal may only specify known properties, 
// and 'c' does not exist in type 'AB'

Playground link to code

It is possible by changing the types:

type Params<T = Record<string, unknown>> = {
  p: T;
  // other metadata
}

type ParamsExtract<T extends (params: Params<any>) => any> = T extends (params: Params<infer U>) => any ? U : never;

type Handler = <Fn extends (params: Params<any>) => any>( // <-- if T is unspecified how can i tell ts to infer it from Params<T>?
  func: Fn,
  input: ParamsExtract<Fn>
) => string;

type Config = {
  handler: Handler
}

const c: Config = {
  handler: (params, input) => 'ok' // <-- no possibility to pass/state the generic parameter here
}


type AB = {a:number, b:number}
const myHandler = (params: Params<AB>) => params.p.a + params.p.b;

c.handler(myHandler, { a:1, b:2, c:"unexpected" }) // error

So the generic becomes the function type and the params get extracted from it. See here.

Related