> But code written from scratch doesn't have that issue. I'd be interested in a counterexample -- is there a code snippet that passes the strict mode of the compiler, and doesn't interoperate with untyped code, but produces an unexpected runtime error?
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.