🔎 Search Terms
this constraint assign type convert method
🕗 Version & Regression Information
- This is the behavior in every version I tried (3.3-5.5), and I reviewed the FAQ for entries about
"Common "Bugs" That Aren't Bugs
⏯ Playground Link
https://www.typescriptlang.org/play/?ts=5.5.3#code/JYOwLgpgTgZghgYwgAgEJwF4B4AqA+ZAbwFgAoASASgjkgAoBKALmRwG4yBfMshAGzgBnQWiEoSFAMQAPAIQsAriADWIAPYB3EG2QB6XcgBiaqEmTqAtqDh9kYAJ4AHUAHMAdDwr7kAQT7AhZBgTZAQ1EEEwKAUEMBMAGmRQAAtoYEgAE2QAI3tkG1tBBWz+IUEIQQ8KR2L-BGRI2mB6qhpIXDw6MGTgQRYQCA1GZABeAhxmViIycnJqMAUoEHNBux7BRg4KblIdsm8cCrAG4tLhXgFhZABlYuQIaUgQDJF0cumpOUUVdS0dbwACtQAG4QcD5QSuVw3O5xIxqNRVPakbytWgoMJQaixPh5OCORz+Cp2JwoGBQNQWE4lS6CXjhSLU0Yw7JuNH0BhbbzIHkAPQA-J4DqkSY4UGoYGtekFEHEoHkEHBltkUIhHAtqFlQPllqBILBEBAqmEIscAJJvCAsdDYS0EEaicpbE2Ms23bLWzBYd32llbemm5DA93Mt3FNnUdGbfYGHnIAUBxnAy2hy0RtoQaMo2N8wWkGOsZK0fJ8QoQCAWEQuBEZRLZBTHDQYpUlwRqCGQlzLboVDG04lwntJECY7FgXGiiBa8DQeBIEQadLJNQN+5YkxuQvS8oVhcUkDuRPmy0AJk92B9zMt-wM12XCj4WXUxzCFmcfBQdHrxwyagqDE3M1TSVedkAlR1VWocw1GOd1jQZc13TPURbTEX13RvZAgTgbIcInNRlClERX3fYkSOAD9AOAkdiXAkM4CgspgC7PCUDhNNPBdY5g2KE9Qzg9lM05As4wTUguKDU9UzEdMo2E7M43jPMj2QC0KWUMFzywSIoFcX1rz0W970faCX0pUjkC-Vdf3-KjGhokRwJTBiUB01x4MDZ8fGuKJXBYNyD1TdSwVkjlMIAYQ-BiJw0fd3F8YRmIiHVh3sswnLEOx2zgBpfIPNwgA
💻 Code
interface Baz<T> {
create(): T;
}
class Base {
#x!: unknown; // Force nominal typing.
// Alias for constructor, inherited by all subclasses.
public static create<T>(this: new() => T): T {
return new this();
}
}
// Test subclass
class Sub extends Base {
#x!: unknown; // Prevent asinging Sub to Foo.
}
// create correctly applies type from subclass
const sub = Sub.create();
// ^?
// The type of this factory can be acptured in an interface.
const IBase: Baz<Base> = Base;
const ISub: Baz<Sub> = Sub;
const vSub = ISub.create();
// ^?
const vBase = IBase.create();
// ^?
// That all seems good, but we can also assign these classes to the incorrectly typed interfaces without error. This seems wrong.
const IBase2: Baz<Sub> = Base; // Should not compile (but does). Instances of Base are not Sub.
const ISub2: Baz<Base> = Sub; // Prabably ok this compiles compile. Instances of Sub are assignable to Base.
const vSub2 = ISub.create();
// ^?
const vBase2 = IBase.create();
// ^?
const IBroken: Baz<string> = Base; // Should not compile (but does). Instances of Base are string.
const notAString: string = IBroken.create(); // Clearly wrong. Assigns an instance of Base to a string.
🙁 Actual behavior
Generic constraints on this
work fine, except for types containing such methods being assignable to interfaces where the method is no longer generic, and the object with the method does not satisfy the usage of this
required in the implementation.
This allows assigning of the object with the generic method to an interface which allows invalid use of the method.
🙂 Expected behavior
When an operation is invalid and forbitten by the type system, I expect an assignment that does not require a cast to not start allowing that previously forbidden operation.
I know TypeScript has specifically chosen to violate this by design in a some cases (for example assignment making read-only fields writable) for compatibility purposes, but this case seems like something that wouldn't intentionally be allowed.
When a type has a member with a method with a generic this constraint, I would expect assigning that type to another type to check that either it has a generic this constraint which is compatible and/or substitute the object type for the generic type parameter.
For example in the above code, when type checking
const IBase2: Baz<Sub> = Base;
I would expect typeof Base
to be provided as the this
in create<T>(this: new() => T): T
resulting in create(this: typeof Base): Base
or just create(): TBase
both of which would fail type checking since neither is not compatible with create(): Sub
Additional information about the issue
In the above example, rephrasing as below still reproduce the same issue:
class Base {
#x!: unknown; // Force nominal typing.
// Alias for constructor, inherited by all subclasses.
public static create<This extends new() => InstanceType<This>>(this: This): InstanceType<This> {
return new this();
}
}
I originally hit this issue when trying to create a minimal repro for a different problem (where similar code errored when it shouldn't). I haven't found a simple repro for that yet, but I can say that this pattern of using this based generic type constraints on statics has been useful in real production code in Fluid-Framework's schema system, where we use classes as schema and their instance types as values. Due to the inability to have multiple constructors in JS, adding static members like this provide a nice way to have more constrained construction APIs that survive subclassing. Its also super handy for providing strongly typed inheritable implementations of Symbol.HasInstance. The only issues I have had with this pattern is when implicitly converting the class objects to interface types, the typing hasn't been accurate.
1条答案
按热度按时间watbbzwu1#
你还记得为什么它是这样工作的吗?