Nan Li
Nan Li

Reputation: 593

in typescript, how to define a specialized version of a generic function?

if I have a generic function:

const f1 = <T>(x: T) => console.log(x)

I can define a specialized version for f1:

const f2 = (x: number) => f1(x)

then typescript will throw an error if I do this:

f2('6')

but f2 is actually another function that calls f1.

Is there a way to define f2 as a specialized version of f1? something like:

const f2 = f1<number> // this doesn't work

Upvotes: 1

Views: 532

Answers (2)

jcalz
jcalz

Reputation: 327624

I'm going to change your example function because the type <T>(x: T)=>void is not doing anything useful with the generic type parameter, and is practically the same as (x: unknown)=>void, a concrete function type. Instead, let's look at a function of type <T>(x: T)=>T, which preserves the input type and outputs a value of the same type, where (x: unknown)=>unknown would not suffice:

const f1 = <T>(x: T) => (console.log(x), x);
// const f1: <T>(x: T) => T

const n: number = 6;
const sixNumber = f1(n); // const sixNumber: number

const s: string = "6";    
const sixString = f1(s); // const sixString: string

What you want to do is take the generic function f1 and use it as if it were a concrete function f2 of type (x: number)=>number. This is a widening of the the type of f1, since the every function of type <T>(x: T)=>T is also a function of type (x: number)=>number but not vice versa. The compiler recognizes this as a valid widening and you can just annotate f2 as the widened type yourself:

const f2: (x: number) => number = f1; // no error
const sixNumberOkay = f2(n); // okay
const sixStringNotOkay = f2(s); // error! string is not number

So that's good.

--

Now, as you noted, what you can't do is take the type of f1 and the type number and automatically produce the type of f2. The type system has no way for you to represent substituting types for type parameters. Neither f1<number> nor typeof f1<number> are valid. TypeScript doesn't have enough support for higher kinded types to express this, at least not purely at the type level.

In TypeScript 3.4, TypeScript introduced improved support for inferring generic functions from other generic functions. So while you can't do the type manipulation you're asking about at the type level alone, you can write some functions that make the compiler do that manipulation anyway. For example:

function specify<A extends any[], R>(f: (...a: A) => R) {
    return () => f;
}

The specify() function takes any function, even a generic one, and returns a new zero-arg function which is also generic, if the original one was. And that function will return the original function when called. That allows you to do this:

const f3 = specify(f1)<number>(); // const f3: (x: number) => number

specify(f1) returns a function of type <T>() => (x: T) => T. And thus specify(f1)<number>() produces the non-generic (x: number)=>number function. And it should work at runtime too:

const sixNumberStillOkay = f3(n); // okay
const sixStringStillNotOkay = f3(s); // error! string is not number

I'm not sure if you're going to be widening generic functions to concrete ones often enough for specify() to be worth it; that's up to you.


Okay, hope that helps; good luck!

Link to code

Upvotes: 2

ThomasThiebaud
ThomasThiebaud

Reputation: 11969

You can define the generic type before like that

type Fn<T = any> = (x: T) => void

const f1: Fn = (x) => console.log(x)
const f2: Fn<number> = f1
f2('6') // Argument of type '"6"' is not assignable to parameter of type 'number'.

Here is a link to a working playground

Upvotes: 1

Related Questions