typescripttype-systemsstructural-typing

Is the TypeScript type system too relaxed in this case?


I think I've encountered a scenario that seems like it should cause the TS compiler to error (but it isn't) and I'm hoping someone can explain why.

In the code below I'm passing an interface Foo to the function frobnicator which it accepts. Then, in the body of frobnicator, I "remove" the field bar.y. After frobnicator terminates, the type system allows me to print bar.y (without error'ing) despite y no longer existing.

Shouldn't the type system forbid me from passing foo to frobnicator because now foo doesn't implement the Foo interface anymore and TS thinks it does?

interface Foo {
    bar: { x: number, y: number };
}

function frobnicator(thing: { bar: { x: number } }) {
    thing.bar = { x: 1 }; // "remove" bar.y
}

const foo: Foo = { bar: { x: 1, y: 2 } };
frobnicator(foo); // The implementation "removes" bar.y
console.log(foo.bar.y); // TypeScript does not error despite bar.y missing

Solution

  • The answer is yes, your code demonstrates an unsound behaviour of Typescript's type system, for exactly the reason you described in your question.

    The answer is also no, you didn't "break" Typescript's type system, because this unsound behaviour is the intended behaviour. Typescript is not totally sound, and is not meant to be totally sound; quoting from the docs:

    TypeScript’s type system allows certain operations that can’t be known at compile-time to be safe. When a type system has this property, it is said to not be “sound”. The places where TypeScript allows unsound behavior were carefully considered, and throughout this document we’ll explain where these happen and the motivating scenarios behind them.

    Typescript's designers explicitly chose a balance between soundness and usefulness; a fully sound type system would not treat {bar: {x: number, y: number}} as a subtype of {bar: {x: number}}, but real Javascript code typically does pass around objects with more properties than the function will actually access, so the strictly sound behaviour would not be very useful to developers who write realistic Javascript code.