TypeScript: Dangerous calls allowed by TS: class generic type, when `unknown`, should be treated as `never` in the class methods/callback parameters
š Search Terms
unknown never generic type methods
š Version & Regression Information
- This is the behavior in every version I tried, and I reviewed the FAQ for entries about _________
⯠Playground Link
š» Code
// allows dangerous calls
{
class X<T> {
callback(t: T) {}
}
const xb = new X<boolean>();
const x: X<unknown> = xb; // ok
x.callback(34); // dangerous.
}
// can't assign if callback
{
class X<T> {
callback!: (t: T) => void
}
const xb = new X<boolean>();
const x: X<unknown> = xb; // nok
x.callback(34); // dangerous
}
// proposal
{
class X<T> {
callback(t: T extends unknown ? never : T) {}
}
const xb = new X<boolean>();
const x: X<unknown> = xb; // ok
x.callback(34); // error (good)
}
š Actual behavior
When a class generic type is unknown, if used as parameters in a method/callback, will authorize dangerous calls :
(x as X<unknown>).callback(34); // we don't know the generic type of x so we should assume it is unsafe to call this method.
Also, due to that, if the class has a callback, we canāt assign X<...> to X<unknown>.
š Expected behavior
TS should assume the method parameter type is never to prevent its call, until the user has asserted the class real type.
// proposal
{
class X<T> {
callback(t: T extends unknown ? never : T) {}
}
const xb = new X<boolean>();
const x: X<unknown> = xb; // ok
x.callback(34); // error (good)
}
Additional information about the issue
This would also enable to use unknown instead of any in some cases :
// use unknown instead of any to not trigger LINT warning.
function foo<T extends Record<string, X<unknown>>>() {
}
Current workaround would be :
function foo<T extends Record<string, X<U>>, U>() {
}
About this issue
- Original URL
- State: closed
- Created 4 months ago
- Comments: 25 (9 by maintainers)
Iām sorry for that, but youāll have to admit that TS has LOT of undocumented unsoundness, at a point you really should have a whole āDesigns choices and intentional unsoundnessā chapter in the documentationā¦
Some features are even undocumented, like
inandoutannotations, only talked about in the 4.7 announcement. With such feature, it should either (from best to worst) :in/outby itself and explicit annotation is used to change this behavior.in outby default.in/outannotationsBut instead of introducing some compiler flag(s), TS introduced unsoundness instead and again⦠This is a living hell of undesirable undocumented behavior⦠just to dirty-solve some specific cases, when more control on the compiler flags would do the trickā¦
If if walk like a bug, and quack like a bug, it is very likely a bug. You can call intentional bugs āintentional unsoundnessā, it still remains a bug (more specifically a flow or a fault in the design).
How many big issues are still open and has been overlooked for 5+ years, while having lot of duplicates ?
Ah, right. Iāve misinterpreted my quick tests. This has already been mentioned in the thread - but well, methods are bivariant in TS. You might not agree with this decision but itās here to stay. You can always create a linting rule to avoid the method syntax altogether. This behaves like you expect it to:
When TS gets it wrong on very basic examples⦠is it really the case ?
At this point, better having a compiler option to enable the second behavior : everything is
in outby default.The fact that bugs are called āunsoundnessā without documenting them is a fault in the design. As I already stated, I can understand little unsoundness on edges cases due to the internal implementation of things. But, by default, allowing implicit
Dog[] -> Animal[]cast isnāt an unsoundness, this is a dangerous behavior.This should:
Dog[] -> readonly Animal[]castThe same for cast with
readonlyproperties.If only this different tool existedā¦
@denis-migdal Iām just going to ask that you please not learn TypeScriptās various ins and outs via filing bugs on the issue tracker. If you have some behavior that also repros in 3.3, itās 99.99% certain that itās not a longstanding bug thatās just been overlooked for 5+ years.
Indeed.
Still, I donāt understand why TS isnāt doing such very basic check. We can very easily implement our own system, and not much needs to be added to TS to make it cleaner and more generic: