Typescript object types are flexible, allowing for extensibility rather than strict closed or sealed structures. This flexibility enables objects to have extra properties not explicitly defined in the type, which is essential when extending interfaces or classes:
interface Foo { bar: string; }
interface Baz extends Foo { qux: number; }
const baz: Baz = { bar: "abc", qux: 123 };
const foo: Foo = baz; // valid
Excess property checking serves as a useful linting feature that warns against assigning an object literal with additional properties to a structure that does not expect them:
const fooDirect: Foo = { bar: "abc", qux: 123 }; // error!
// ---------------------------------> ~~~~~~~~
// Object literal may only specify known properties, and
// 'qux' does not exist in type 'Foo'.
While excess property warnings may cause confusion, they do not impact type safety significantly. They highlight potential mistakes or oversights but can be bypassed by using intermediate variables or scenarios where warnings are not enforced.
In cases like:
type P =
{ foo: boolean, bar?: undefined } |
{ foo: boolean, bar: boolean, baz: boolean };
const p: P = { foo: true, baz: true };
The assignment remains valid even if an excess property warning is triggered. To prevent such assignments, you can introduce optional properties with the `never` type to ensure missing or `undefined` properties, effectively restricting unexpected keys:
type P =
{ foo: boolean, bar?: undefined, baz?: never} |
{ foo: boolean, bar: boolean, baz: boolean };
const p: P = { foo: true, baz: true }; // error
This approach enforces the desired restriction without relying solely on excess property warnings.
Although the behavior observed might be considered an issue in TypeScript (as reported at microsoft/TypeScript#39050), it's primarily related to excess property warnings within discriminated unions. Future changes could align this behavior with expectations, especially concerning optional discriminant properties.
To ensure stricter validation, making discriminant properties required helps enforce constraints, leading to errors when attempting invalid assignments:
type P =
{ foo: boolean, bar: 0 } | { foo: boolean, bar: 1, baz: boolean };
const p: P = { foo: true, bar: 0, baz: true }; // desired error
// -----------------------------> ~~~~~~~~~
const indirect = { foo: true, bar: 0, baz: true } as const;
const pIndirect: P = indirect; // no error
Considering these nuances, utilizing the optional-`never` approach alongside potential future updates in TypeScript ensures robustness in handling object type assignments.
Explore the code on TypeScript Playground