Search code examples
typescriptgenerics

TypeScript does not validate generic type?


I'd like to use an async helper function in my TypeScript code that returns a database query builder instantiated for a specific database model type. However, I noticed that TypeScript does not check the specified generics type and so I am able to assign the return value to a variable where the generic type is wrong.

As a simplified example this is valid:

class Foobar<T> { }

const asyncFunc = async (): Promise<Foobar<string>> => {
    ...
}    
const caller = async (): Promise<void> => {
  const result: Foobar<number> = await asyncFunc();    
}

The type of Foobar is string in the return type of asyncFunc and it is number in the type of the variable "result".

Is this expected behaviour and is there a reason behind this? Is there a way around this?

This is quite annoying for my database code as TypeScript does not now check what I'm trying to insert with my query builder.


Solution

  • TypeScript's type system is (largely) structural and not nominal; two types with the same shape or structure are considered the same type, even if they have different names or declarations. Since the following generic class declaration

    class Foobar<T> { }
    

    does not use the type parameter T anywhere structurally inside the class body, then Foobar<T> is structurally independent of T. So the types Foobar<string> and Foobar<number> are the same. Indeed the class body is (at least in your example) completely empty, and thus Foobar<T> is equivalent to the empty object type {} no matter what T is.

    Empty classes and unused type parameters often lead to surprising results for those used to nominal type systems (such as Java's) and are therefore best avoided. See the TypeScript FAQ entries Why do these empty classes behave strangely? and Why is A<string> assignable to A<number> for interface A<T> {}? for more information.

    Assuming you wanted Foobar<T> to depend on T, you can achieve this by adding some property or method to Foobar<T> that actually depends on T. For example:

    class Foobar<T> {
      prop: T;
      constructor(prop: T) {
        this.prop = prop;
      }
    }
    
    declare const asyncFunc: () => Promise<Foobar<string>>;
    
    const caller = async (): Promise<void> => {
      const error: Foobar<number> = await asyncFunc();
      // Type 'Foobar<string>' is not assignable to type 'Foobar<number>'.
      const okay: Foobar<string | number> = await asyncFunc(); // okay
    }
    

    Now Foobar<string> is not assignable to Foobar<number>, because the prop property string is not assignable to number. Note that object types are covariant in their property types (see Difference between Variance, Covariance, Contravariance and Bivariance in TypeScript) so that means Foobar<string> is still assignable to Foobar<string | number>, because string is assignable to the union type string | number. If this is undesirable, you might need to modify the way Foobar<T> depends on T. But we're getting out of scope for the question as asked, so I'll stop there.

    Playground link to code