Closed
Description
TypeScript Version: 2.2.2
Code
interface List<T> {
add(elem: T): void
}
function map(list: List<number>): List<number | null> {
return list;
}
function foo(list: List<number>) {
map(list).add(null);
}
Expected behavior:
Compiler should complain about line 6 (return list
) when strictNullChecks
is enabled, because add(elem: number): void
has a different signature than add(elem: (number | null)): void
.
Actual behavior:
This above code goes through the compiler, even though strictNullChecks
, noImplicitAny
et c. are all enabled. This results in that we can add null
to a List<Number>
in function foo
and the compiler does not complain.
Metadata
Metadata
Assignees
Type
Projects
Milestone
Relationships
Development
No branches or pull requests
Activity
ikatyang commentedon Jul 28, 2017
It is different indeed, but it is assignable too. Why not just delete the
null
from return type so that it'll throw an error on.add(null)
?mckauf commentedon Jul 28, 2017
In fact, I discovered this when investigating a type bug in a bigger application where the TypeScript compiler should have complained about a type mismatch but didn't.
The above code is not the actual use case, but just the minimal example that produces the same error.
Yes, I could delete the
null
from the return type in the above example but this would not solve the actual bug in the compiler ;)Why do you think that both types are assignable?
kitsonk commentedon Jul 28, 2017
List<number>
is assignable toList<number | null>
... that does not contravene strict null checks or the type system at all. Just asnumber
is assignable tonumber | null
. You can always widen a type explicitly.mckauf commentedon Jul 28, 2017
Yes,
number
is assignable tonumber | null
, but with generics this is not be the case. I.e.,X<number>
should not be assignable toX<number | null>
. Because otherwise you can construct cases where this results innumber | null
arguments being mapped tonumber
arguments, butnumber | null
is not assignable tonumber
.The above example illustrates this very well: we can suddenly add
null
elements to a list that normally should contain onlynumber
elements, and this does not require an explicite cast or anything. If this really is the expected behavior, then I would call the type system broken, because it does not guarantee type safety.They recognized this problem in Java as well, and this is exactly the reason why you can assign
Float
toNumber
, but you cannot assignList<Float>
toList<Number>
in Java; namely this would result in that you can addDouble
values to aList<Float>
.gcnew commentedon Jul 28, 2017
The problem is that TypeScript doesn't support variance annotations (#1394). If the return list's parameter had been marked as covariant
List<out number | null>
there would have had been no problem, as it would have had been read only.kitsonk commentedon Jul 28, 2017
If you are explicit about types... While you don't consider it a cast, you are relying upon asserted types and TypeScript is checking that the types are assignable, which they are. I don't see how it become un-type safe if you explicitly widen a type.
gcnew commentedon Jul 28, 2017
@mckauf Java has use-site variance declarations.
mckauf commentedon Jul 28, 2017
Because casting
X<number>
toX<number | null>
, has nothing to do with widening; they are just two incompatible types; unlike castingnumber
tonumber | null
, which is indeed widening.I have pointed out why. You can also read this to understand the problem: http://onewebsql.com/blog/generics-extends-super
In Java there are boundaries for type parameters (
extends
,super
), which are similar to the covariant type parameters gcnew mentioned (in
,out
). Too bad Typescript doesn't support them. But even if it doesn't, the compiler cannot simply assume thatX<number | null>
includesX<number>
; this is just wrong.RyanCavanaugh commentedon Jul 28, 2017
See #1394.