admin管理员组

文章数量:1125727

class Token<T> {
  public name: T;
  constructor(name: T) {
    this.name = name;
  }
}

class LazyToken<T> {
  public callback: () => GenericToken<T>;
  constructor(callback: () => GenericToken<T>) {
    this.callback = callback;
  }

  public resolve() {
    return this.callback();
  }
}

type GenericToken<T = unknown> = Token<T> | LazyToken<T> | Newable<T>;

type Newable<
  TInstance = unknown,
  TArgs extends unknown[] = any[]
> = new (...args: TArgs) => TInstance;


function get<T>(token: GenericToken<T>, options: any = {}): T | undefined {
  return 123 as T;
}

class Test1 {

}

class Test2 {
  public name = 123;
}

class Test3 {
  sayHello() { }
}

class Test4 {
  public hello = 123;
}

class Test5 {
  public hello = 123;
  sayHello() { }
}

const test1 = get(Test1); // Test1 | undefined
const test2 = get(Test2); // test2: string | undefined
const test3 = get(Test3); // test3: string | undefined
const test4 = get(Test4); // test4: string | undefined
const test5 = get(Test5); // test5: string | undefined

Playground

The type of test1 is Test1 | undefined as I expect. But test2 through test5 are all of type string | undefined, instead of Test2 | undefined through Test5 | undefined, which I don't expect. How can I fix it?

class Token<T> {
  public name: T;
  constructor(name: T) {
    this.name = name;
  }
}

class LazyToken<T> {
  public callback: () => GenericToken<T>;
  constructor(callback: () => GenericToken<T>) {
    this.callback = callback;
  }

  public resolve() {
    return this.callback();
  }
}

type GenericToken<T = unknown> = Token<T> | LazyToken<T> | Newable<T>;

type Newable<
  TInstance = unknown,
  TArgs extends unknown[] = any[]
> = new (...args: TArgs) => TInstance;


function get<T>(token: GenericToken<T>, options: any = {}): T | undefined {
  return 123 as T;
}

class Test1 {

}

class Test2 {
  public name = 123;
}

class Test3 {
  sayHello() { }
}

class Test4 {
  public hello = 123;
}

class Test5 {
  public hello = 123;
  sayHello() { }
}

const test1 = get(Test1); // Test1 | undefined
const test2 = get(Test2); // test2: string | undefined
const test3 = get(Test3); // test3: string | undefined
const test4 = get(Test4); // test4: string | undefined
const test5 = get(Test5); // test5: string | undefined

Playground

The type of test1 is Test1 | undefined as I expect. But test2 through test5 are all of type string | undefined, instead of Test2 | undefined through Test5 | undefined, which I don't expect. How can I fix it?

Share Improve this question edited 2 days ago jcalz 326k29 gold badges433 silver badges435 bronze badges asked 2 days ago threetreethreetree 739 bronze badges 8
  • I have posted the code in the question. – threetree Commented 2 days ago
  • Since you cast a string argument to T and your generic isn't constrained to extend string this is all completely pointless and you could just not use typescript at all, so it isn't really clear what your intention is or why it's not working the way you expect. What are you actually trying to accomplish here? – Jared Smith Commented 2 days ago
  • In fact, I plan to use Token like this, that is, to reference another type through a string. Here the ide konws testInst's type is TestClass. const token = new Token<TestClass>('a test token'); const testInst = get(token); – threetree Commented 2 days ago
  • The normal implementation of the Token class should be like this. class Token<T>{public name:string; constructor(name:string){this.name=name;}} But the ide will complain that the T type is not used in the implementation of Token. Can you tell me how to implement Token in the best practice? – threetree Commented 2 days ago
  • 1 Of course, please edit it. – threetree Commented 2 days ago
 |  Show 3 more comments

1 Answer 1

Reset to default 2

The problem is that there is an unexpected ambiguity in your types. The type Token<string> corresponding to an instance of the Token class where T is string is structurally equivalent to {name: string}. And this turns out to be a supertype of the Function interface, which (at least for ES2015 and later) is declared in TypeScript's library to have a name property of type string. So, as far as TypeScript is concerned, everything with a string-valued name property is also a Token<string>. So that includes all Functions, which includes all class constructors, like typeof Test1, typeof Test2, et cetera. Yes, that's weird, but it's a consequence of TypeScript's structural type system. If you're used to nominally typed languages like Java or C# where such things simply cannot happen, then you might object that of course a class constructor is not an instance of Token. But TypeScript is only looking at the shape of the type, not where it is declared.

So when you try to infer T from a value of type GenericToken<T> which is a class constructor, then it is ambiguous. It could either decide that the GenericToken<T> is a Newable<T> and infer T as the instance type of the class constructor, or it could decide that the GenericToken<T> is a Token<string> and infer T as string. The particular details of when TypeScript chooses one over the other might be interesting, but I don't think you can rely on them. It's best to avoid the problem, rather than try to really understand why typeof Test1 is inferred as a Newable<Test1> while the rest of them are inferred as a Token<string>. It's ambiguous, and TypeScript chooses one, and it's not always what you want. (See this FAQ entry for a similar issue with inference.)

The way to fix the problem is to break the structural compatibility between Function and Token<string>. I'd do this by adding any property to Token that you don't expect to see in the wild. (You could also add a private property to Token, since that will never conflict.) For example:

class Token<T> {
  anyOtherProp = true; // <-- this could be just about anything
  public name: T;
  constructor(name: T) {
    this.name = name;
  }
}

Now there's no ambiguity. A class constructor is very unlikely to be a Token<string> (it would need to have a static anyOtherProp property of type boolean), so you get the inference you expect in all the cases:

const test1 = get(Test1); // Test1 | undefined
const test2 = get(Test2); // Test2 | undefined
const test3 = get(Test3); // Test3 | undefined
const test4 = get(Test4); // Test4 | undefined
const test5 = get(Test5); // Test5 | undefined

Playground link to code

本文标签: typescriptWhy are generics not working properly hereStack Overflow