You'd think so, right? But no, typescript is deliberately unsound in ways that have nothing to do with gradual typing. Here are a few examples.
Signatures written in method syntax are bivariant, which is not correct
interface Unsound {
f(x: number | string): number
}
interface Unsound2 {
f(x: number): number
}
const a: Unsound2 = { f: (x: number) => x }
const b: Unsound = a
const c: number = b.f("not a number")
Type predicate results survive mutation const hasA = (x: object): x is { a: unknown } => "a" in x
const deleteA = (x: { a: unknown }) => {
delete x.a
}
const unsound = (x: object) => {
if (hasA(x)) {
deleteA(x)
return x.a
} else {
return "no a"
}
}
Many stdlib types are incorrect. JSON stuff is particularly bad: JSON.parse and Body.json() both return `any`.You can spread things that aren't objects
const unsound = <X,Y>(x: X, y: Y): X & Y => ({...x, ...y})
const bad: never = unsound(5, 4)
(And even for objects, `X & Y` is not the correct type when you have overlapping keys)Anything with optional fields can be widened incorrectly
const unsound = <T extends { x: number }>(t: T): { x: number, y?: number } => t
const bad: number | undefined = unsound({ x: 5, y: "not a number" }).y
Assignment doesn't handle `readonly` properly interface Readonly {
readonly x: number
}
interface Mutable {
x: number
}
const a: Readonly = Object.freeze({x: 5 })
const b: Mutable = a
b.x = 4
> The OPERATION is legal, but the data isn't, which isn't something that any type system will tell you.There are some that will, though unfortunately none that are really production-ready yet.