Search code examples
typescriptgenericstypescript-generics

How to derive the type on a generic class that is constrained to primitives in TypeScript?


I want to constrain my generic Foo class to accept only certain types. Also, I need to auto-derive the type by the default value that is passed to the constructor of Foo.

With just Foo<T>, when I call new Foo("bar") the type is Foo<string>. That is what I want.

However, when I try to restrict type T as seen below, when I call new Foo("bar") the type is Foo<"bar">. Is there a way to make the compiler to still derive string?

Works nice:

  class Foo<T> {
    constructor(private value: T) { }
  }
  
  const foo = new Foo("bar"); // this derives type Foo<string> for foo

My Problem:

  type FieldTypes = string | number | boolean | Date | null;

  class Foo<T extends FieldTypes> {
    constructor(private value: T) { }
  }
  
  const foo = new Foo("bar"); // this is of type Foo<"bar">. Why?

This should also be derived to Foo<string>, without explicitly defining

const foo:Foo<string> = new Foo("bar"); .

Is there a better way to constraint my Foo Types?


Solution

  • That narrowing is intentional, as per microsoft/TypeScript#10676. By constraining T to a type including string, TypeScript takes that as a hint that you probably want string literal types. That is often exactly what people want, but in your case it's not.

    There are lots of possible workarounds here, but the easiest one is to not constrain T at all directly, but just force usages of T to be assignable to FieldTypes, such as by using an intersection:

    class Foo<T> {
        constructor(private value: T & FieldTypes) { }
    }
    
    const foo = new Foo("bar"); 
    // const foo: Foo<string>
    

    This might meet your needs, or it might give you trouble later. But it's easy.


    More complicated is to just take over the way the constructor signature works, by writing an explicit literal-widening utility type

    type Widen<T> = 
      T extends string ? string : 
      T extends number ? number : 
      T extends boolean ? boolean : 
      T;
    

    and renaming Foo out of the way

    class _Foo<T extends FieldTypes> {
        constructor(private value: T) { }
    }
    

    and redefining Foo to be a type and a constructor value, where the constructor explicitly returns a widened version of the type argument:

    type Foo<T extends FieldTypes> = _Foo<T>;
    const Foo = _Foo as new <T extends FieldTypes>(value: T) => Foo<Widen<T>>;
    

    It behaves the same way:

    const foo = new Foo("bar");
    // const foo: Foo<string>
    

    But it's cumbersome so without more detailed use cases, I'd go with the easy approach first.

    Playground link to code