Home > OS >  Passing a generic type to a function in typescript
Passing a generic type to a function in typescript

Time:10-04

Consider example:

interface A {
   foo: string;
}

interface B {
   foo: string;
}

function a<T>() {
   function b(arg: T) {
      return arg.foo;
            ^^^^^^ Property 'foo' does not exist on type 'T'.
   }

   return b;
}

a<A>();
a<B>();

How to make it work so I dont have to hardcore the type like:

function b(arg: A | B)

So I will be able to pass the type as a generic so if there's like 5 or even more different types I will not have to do it like A | B | C | D | E... but just as a generic?

CodePudding user response:

you can try something like

interface A {
   foo: string;
}

interface B extends A {
   
}

function a<T extends A>() {
   function b(arg: T) {
      return arg.foo;
           
   }

   return b;
}

a<A>();
a<B>();

CodePudding user response:

In order for you to be able to access arg.foo, the compiler must be convinced that, at the bare minimum, arg might actually have a property named foo. So the type of arg must be assignable to something like {foo?: unknown}, where foo is an optional property and the type of the property is the unknown type.

It's not clear from your example if you want to make something stricter than this (say that it definitely has a property named foo, or that the value at that property must be a string). For now, I'm only going to assume that you want to access arg.foo without an error.

If you are using the generic type parameter T as the type of arg, then T must be constrained to something assignable to {foo?: unknown}.

For example:

function a<T extends { foo?: unknown }>() {
    function b(arg: T) {
        return arg.foo; // okay, no error
    }    
    return b;
}

This saves you from having to write T extends A | B | C | ..., assuming that all of the A, B, C, etc... types are themselves assignable to { foo?: unknown }. Your given A and B types are assignable, so the following works:

a<A>();
a<B>();

Do note that your A and B types from the example are actually identical. TypeScript's type system is structural. Because types A and B have the same structure, they are the same type. It doesn't matter that there are two declarations and two different names:

let aVal: A = { foo: "a" };
let bVal: B = { foo: "b" };
aVal = bVal; // okay
bVal = aVal; // okay

So writing T extends A would be fine if you want the compiler to enforce that foo exists and is a string; it wouldn't stop B from working. A structural type system frees you from having to anticipate all possible named types:

function a2<T extends A>() {
    function b(arg: T) {
        return arg.foo.toUpperCase();
    }
    return b;
}
    
a2<A>(); // okay
a2<B>(); // okay

interface C {
    foo: string;
    bar: number;
}
a2<C>(); // okay

a2<Date>(); // error
// ~~~~
// Property 'foo' is missing in type 'Date' but required in type 'A'.

If you care about "hardcoding" the name A, you can use an anonymous type like {foo: string} as in T extends {foo: string}, but that doesn't really change much from the type system's perspective, since A and {foo: string} are the same type.

Playground link to code

  • Related