typescript difference between placement of generics arguments

There are two distinct flavors of generics in TypeScript: generic functions and generic types.


A generic function declares its generic type parameter (or parameters) on the call signature of the function:

type IdentityGenFunc = <T>(t: T) => T

The type parameter on a generic function (T above) isn’t specified until the function is actually called, at which point the caller specifies it (or the compiler infers it on behalf of the caller). This means that a generic function implementation must be able to deal with any possible specification of T that the function caller wants:

const idGenFunc: IdentityGenFunc = x => x;

const resultABC = idGenFunc("ABC"); // T inferred here as ABC
// const resultABC: "ABC"
const result123 = idGenFunc(123); // T inferred here as 123
// const result123: 123

A generic type declares its generic type parameter (or parameters) on the type declaration:

type IdentityGenType<T> = (t: T) => T

The type parameter on a generic type (T above) must be specified before you can have a value of that type. If a generic type has a call signature or method that refers to the type parameter, that type parameter is fixed as soon as the surrounding generic type is specified, before that method is called. Once you are talking about, say, an IdentityGenType<string>, then its call signature only needs to be able to handle a string. And so to implement that function you do not need to deal with any possible value of T:

const idGenTypeString: IdentityGenType<string> = x => x + "!";

const resultABCagain = idGenTypeString("ABC"); // okay, "ABC" is assignable to string
// const resultABCagain: string
const result123Again = idGenTypeString(123); // error! 123 is not assignable to string

These two flavors of generics are related to each other. The type IdentityGenFunc is essentially an “infinite intersection” of IdentityGenType<T> for every possible T. The TypeScript type system isn’t quite expressive enough to say that:

// the following is not valid TS, but this is what you want to say
type IdentityGenFunc = forall T, IdentityGenType<T>; // not valid TS, error

but you can witness it by assignments:

// every IdGenFunc is also an IdGenType<number>
const idGenTypeNumber: IdentityGenType<number> = idGenFunc; // okay

On the other hand, you can think of IdentityGenType<T> as the same as IdentityGenFunc instantiated with some type T. Again, the type system won’t let you express this:

// the following is not valid TS, but this is what you want to say
type GenType<T> = instantiate GenFunc with T; // not TS, error

Although TypeScript 4.7 will introduce instantiation expressions which will let you instantiate the type parameters on a value whose type is a generic function:

// if you instantiate a value of type IdGenFunc with Date, then you get an IdGenType<Date> 
const idGenTypeDate: IdentityGenType<Date> = idGenFunc<Date>; // okay for TS4.7+

Playground link to code

Leave a Comment