TypeScript 隐式向上转型静态方法,带有泛型约束的this允许不正确的操作类型,

bkhjykvo  于 5个月前  发布在  TypeScript
关注(0)|答案(1)|浏览(66)

🔎 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

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.

watbbzwu

watbbzwu1#

你还记得为什么它是这样工作的吗?

相关问题