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
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.