Skip to content

TS allows undefined properties on class instance to be called or assigned to variables that don't have undefined in their type #40644

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
ghost opened this issue Sep 19, 2020 · 7 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@ghost
Copy link

ghost commented Sep 19, 2020

Now this was mildly interesting, I hadn't expected this at all. This seems like an odd edge case.
I randomly when over to create a wrapper around the WebAssembly.Table class and add some extra features. I quickly noticed something weird was going on, it took a minute, but then I found out that my class's prototype is being completely ignored. All methods are erased or never applied. TS wasn't catching it either.

In truth, this should fall under an issue about classes that don't return an object whose prototype is the class.

This occurs wherever the expression new T instanceof T is false.

TypeScript Version: Nightly

Search Terms: Undefined cast, wasm table, WebAssembly Table, undefined iterators, classes not returning themselves
(Note: you won't find anything)

Code

class Table extends WebAssembly.Table { // Wasm table doesn't like being inherited from
	*[Symbol.iterator]() {
		const { length } = this;

		for ( let i = 0; i < length; ++i ) {
			yield this.get(i);
		}
	}

	get last() {
		return this.get( this.length - 1 );
	}
};

const tbl = new Table( {
	initial: 15,
	element: "anyfunc"
} );

console.log( tbl instanceof Table ); // root of the problem

const { last } = tbl;

console.log( last, typeof last ); // definitely Function | null

const func: Function | null = tbl.last;

console.log( typeof func );

console.log( [ ...tbl ] );

Expected behavior:

TSC throws type errors about the properties not being on my variable "tbl," and the object not having an iterator.

Actual behavior:

TSC lets the code blindly pass unaffected, resulting in runtime errors.

Playground Link: (Sorry, my internet can't load it at the moment)

Related Issues: No (I'm sure there are, I couldn't find anything about the class problem)

@ghost
Copy link
Author

ghost commented Sep 19, 2020

Maybe related to:
https://github.com/microsoft/TypeScript/wiki/FAQ#why-doesnt-extending-built-ins-like-error-array-and-map-work
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget

Yes! That is precisely the issue, but the actual problem here is that TS doesn't catch those cases. Yeah, they're definitely edge-cases, but they should be caught nonetheless. That was also how I solved my problem, I used Reflect.setPrototypeOf(super(...), Table);.

@AviVahl
Copy link

AviVahl commented Sep 20, 2020

I personally like the Object.setPrototypeOf(this, new.target.prototype); variant that can be copy-pasted as-is (from one edge case to another). I wonder why the wiki doesn't offer that one.

Regarding TypeScript "catching" those cases... There's an open proposal for runtime handling: #15397

@RyanCavanaugh RyanCavanaugh added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Sep 21, 2020
@RyanCavanaugh
Copy link
Member

We don't have any syntax that describes classes that can't be inherited from without extra work. Few enough of these exist, and have enough other issues that are well outside the scope of the type system, that we don't foresee adding additional design space to handle them.

@ghost
Copy link
Author

ghost commented Sep 21, 2020

We don't have any syntax that describes classes that can't be inherited from without extra work. Few enough of these exist, and have enough other issues that are well outside the scope of the type system, that we don't foresee adding additional design space to handle them.

Might I at least suggest that we allow different types on constructors?

class T extends H {
    constructor (...): H {
        ...
    }
}

This is sometimes useful and more accurate. It definitely doesn't solve the inheritance issue, but if annotated on the constructor, it still allows TSC to catch misuse, as H doesn't have the prototype of T.

@RyanCavanaugh
Copy link
Member

Yep, see #27594

@ghost
Copy link
Author

ghost commented Sep 21, 2020

Ooh, that seems to be going well. But should I just close this? As it seems like it simply won't be "fixed." (That is, if we can say that it is objectively "broken")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

2 participants