Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Required vs inferred generic types in TypeScript

What is the difference between following generic types:

type FnWithRequiredParam<T> = (t: T) => void
type FnWithParamInferred = <T>(t: T) => void

As far as I understand, the FnWithRequiredParam will always fail if not explicitly given the generic type in any context. Passing the generic (which is enforced), e.g. FnWithRequiredParam<string> will basically turn it into (t: string) => void in all contexts.

However, I can't find the meaning of the FnWithParamInferred. In some contexts <T> gets inferred from the place it's used (such as Array.map), but the following line throws an error:

var f: FnWithParamInferred = (a: number) => { console.log(a) }

saying that number and T are incompatible. In above line, what is actually T? It's never been declared precisely and is being compared to another type. What are the rules for determining what is the generic T defined in function types like <T>(...) => ...?

Seems that, if <T> is defined as a required generic of a class/interface, e.g. an Array<T>, then methods od array can successfully infer T. But if it's outside of a class/interface, type inference doesn't seem to work.

like image 892
ducin Avatar asked Jan 16 '19 21:01

ducin


1 Answers

The two are very different in the function signature they define.

  • The first defines a regular function signature, that can be customized with a type parameter when used, and that type becomes is fixed into the signature
  • The second defines a generic function signature, that is a function that can accept a parameter of any type T, with T being inferred (or specified explicitly) when the function is called.

Consider the following declarations:

declare const fn: FnWithRequiredParam<number> 
declare const genericFn: FnWithParamInferred;

// T was fixed on declaration 
fn(1) // ok
fn("1") // err  

// T is decded by the caller
genericFn(1) // ok T is number for this call
genericFn("1") // ok T is string  for this call
genericFn<number>("1") // err T was specified as number but string was passed in 

The reason for the error you are getting is that you are attempting to assign a function with a number parameter to a function that should accept a parameter of any type T, with T to be decided by the caller of the function. Only a generic function can satisfy the type FnWithParamInferred

var f: FnWithParamInferred = <T>(a: T) => { console.log(a) }

I think what you really want is to be able to omit the explicit type argument from the variable declaration and have it be inferred based on the value assigned to it. Typescript does not support this. If you define a type annotation for a variable no inference will be done for the variable.

You can omit the type annotation altogether to let the compiler infer the function type:

var f = (a: number) => { console.log(a) } // inferred as (a: number) => void

Or you can define a generic helper function to infer T, but restrict the function signature based on FnWithRequiredParam

function createFunction<T>(fn: FnWithRequiredParam<T>) {
    return fn;
}

var f = createFunction((a: number) => { console.log(a) }) // inferred as FnWithRequiredParam<number>
like image 177
Titian Cernicova-Dragomir Avatar answered Oct 12 '22 11:10

Titian Cernicova-Dragomir