简体   繁体   中英

Static Factory Method Return Type of Generic Class

I'm trying to infer the correct typings for a Static Factory Method for a family of classes, where the parent class is generic. I want the return typing of the static factory method to be the parent class to abstract both children, but typescript infers an or type of both child classes.

abstract class Parent<T> {
  abstract readonly property: T
}

class ChildA implements Parent<string> {
  constructor(readonly property: string) {}
}

class ChildB implements Parent<number> {
  constructor(readonly property: number) {}
}

class Factory {
  public static create(guard: any) /** I want to the return type be only Parent without indicate in a explicit way the generic **/ {
    if (typeof guard === 'string') {
      return new ChildA(guard)
    }
    if (typeof guard === 'number') {
      return new ChildB(guard)
    }

    return new UnkwonClass()
  }
}

I don't know how to describe the factory signature to only return Parent to abstract both child classes, because both will have the same shape, and not have an or type ChildA | ChildB ChildA | ChildB

I've tried to write the signature as Parent, then typescript tells me, that Parent is generic, then I change the signature of the create method to public static create<T>(guard: any): Parent<T> but I have to pass from the instance the type, and I want that ts infer the type that I passed to the children classes.

Playground

I think the issue you are running into is that the TypeScript compiler is currently unable to use control flow analysis to narrow generic type parameters like T in the following code:

class Factory {
  public static create<T>(guard: T): Parent<T> {

    if (typeof guard === 'string') {
      return new ChildA(guard); // error!
      // Type 'ChildA' is not assignable to type 'Parent<T>'
    }

    if (typeof guard === 'number') {
      return new ChildB(guard); // error!
      // Type 'ChildB' is not assignable to type 'Parent<T>'
    }

    throw new Error('Unknown class')
  }
}

When you inspect typeof guard === "string" the compiler can narrow the apparent type of guard from T to T & string . But this does not cause the compiler to say that T itself is now string . Type guarding narrows the types of values , not generic type parameters . And since T is not narrowed to string , the type Parent<T> is not narrowed to Parent<string> , and so ChildA is not seen as assignable to Parent<string> .


There are various open issues in GitHub asking for some improvement here. A good one to start with is microsoft/TypeScript#33014 , which requests that the compiler narrow type parameters via control flow analysis, at least to allow for certain property lookups. Neither this nor related suggestions have been implemented yet, as of TypeScript 4.2... and it's not clear when or even if anything will change here.


Until and unless something changes, my suggestion is to do what you can always do when you know more about the type of some value than the compiler does: use a type assertion . You know that ChildA will be assignable to Parent<T> when typeof guard === "string" , so just tell the compiler that:

class Factory {
  public static create<T>(guard: T): Parent<T> {

    if (typeof guard === 'string') {
      return new ChildA(guard) as Parent<typeof guard>; // okay
    }

    if (typeof guard === 'number') {
      return new ChildB(guard) as Parent<typeof guard>; // okay
    }

    throw new Error('Unknown class')
  }
}

This resolves the error. (Note that your actual code may still yield an error when you write value as Type if the compiler does not see Type as sufficiently related to typeof value . If so, you can still do the assertion via value as unknown as Type or value as any as Type .)

Just be careful with type assertions, since it is now your responsibility to verify type safety in those lines. The compiler can't do it, and if you are wrong about your assertion, the compiler isn't in a position to notice. Accidentally lying to the compiler could lead to weird behavior at runtime. So double check that new ChildA(guard) and new ChildB(guard) are really and truly of type Parent<T> before asserting.

Playground link to code

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM