Home > Software engineering >  Can a generic const call another generic?
Can a generic const call another generic?

Time:01-21

Consider the following Typescript using generics:

//Assume we've got data coming from somewhere we cannot control the type
const fetchFromSomewhere: (str: any) => any = (str: any) => str

const fetchOne: <T>(str: string) => T = (str: string) => {
    const a = fetchFromSomewhere(str)
    return a
}

//Does not work
const fetchTwo: <T>(str: string) => T = (str: string) => {
    const a = fetchOne<T>(str) //Cannot find name 'T'.ts(2304)
    return a
}

//Works
function fetchThree<T>(str: string): T {
    const a = fetchOne<T>(str) //Can use <T> here
    return a
}

fetchTwo fails to compile, saying Cannot find name 'T'.ts(2304). Leaving out the <T> results in its return type being unknown

fetchThree works fine and is a suitable workaround. But is there a way to implement fetchTwo, using a const instead of a function?

CodePudding user response:

If you want to annotate the call signature of an arrow function expression in TypeScript so that it is generic, you do so by putting the type parameter declaration immediately before the argument list (and this list must be in parentheses even if there's exactly one of them). For example, you could turn

const idArrow = x => x;

into

const idArrow = <T>(x: T) => x;

which is analogous to the following function statement:

function idStatement<T>(x: T) { return x };

Note that if you are using JSX or your IDE is expecting JSX, the above arrow function might confuse it. After all, JSX expressions look like HTML tags, and <T> looks like that too. In order to prevent such confusion, you can add a trailing comma after the type parameter (lists of type parameters allow trailing commas):

const idArrow = <T,>(x: T) => x; 

This is completely equivalent to the prior definition of idArrow, except that this one will work even if your compiler settings are set to allow JSX.


Anyway, that means you can do this:

const fetchOkay = <T,>(str: string) => {
    const a = fetchOne<T>(str)
    return a
}

You can't use a T parameter inside the body of an arrow function if it's not declared in the call signature:

const fetchBad = (str: string) => {
    const a = fetchOne<T>(str) //Cannot find name 'T'.ts(2304)
    return a
}

This latter situation is exactly what was happening in your fetchTwo.


And note that arrow function expressions are just expressions; they don't have to be assigned to variables. If you do assign one to a variable, then you can independently choose to annotate the variable or not. That is, you can annotate:

const v: Type = init;

If you do this, the type annotation for the variable lives entirely on the left side of the assignment operator (=), and the initializing expression doesn't get directly involved with that.

In your fetchTwo declaration, you were annotating the fetchTwo variable with a generic function type:

const fetchTwo: <T>(str: string) => T = ...;

and your initializing value was the problematic arrow function before, where T was not in scope.

(str: string) => {
    const a = fetchOne<T>(str) //Cannot find name 'T'.ts(2304)
    return a
}

Hopefully now you can see the disconnect. Personally I would leave off the type annotation and let the compiler infer the type of the fetchTwo variable. But if you do want to annotate, it means you need to annotate both the variable and the call signature of the function expression, separately:

const fetchTwo: <T>(str: string) => T = <T,>(str: string) => {
    const a = fetchOne<T>(str)
    return a
}

and those really are separate; you could rename function parameters and type parameters in the function expression without having it affect the variable type annotation:

const fetchTwo: <T>(str: string) => T = <U,>(xxx: string) => {
    const a = fetchOne<U>(xxx)
    return a
}

Playground link to code

  •  Tags:  
  • Related