Typescript Objects Can Have More Properties Than Declared
Objects in Typescript are considered open or extensible, not closed or sealed. This means that a type can have additional properties beyond what is specified in its declaration. Simply because a property is not explicitly mentioned in the type does not mean it cannot exist in a value of that type. This inability to create "exact" object types was discussed in more detail on microsoft/TypeScript#12936.
For instance:
interface Y {
y: number;
}
Although a value of type Y
must include a property y
of type number
:
let y: Y;
y = {}; // error, Property 'y' is missing in type '{}' but required in type 'Y'.
y = { y: "oops" }; // error, Type 'string' is not assignable to type 'number'.
y = { y: 123 }; // okay
It may appear that adding other properties is restricted. However, assigning an object literal with extra properties results in an error due to excess property checking. But this check serves as more of a linter rule for object literals only and not all values:
const xy = { x: "oops", y: 123 };
y = xy; // okay, no error
This assignment is valid because xy
retains the x
property, making it accessible somewhere. When assigned as y = xy
, xy
functions as a value rather than an object literal, thus avoiding an error.
In practice, allowing excess properties is necessary for extending interfaces as intended:
interface XY extends Y {
x: string;
}
const val: XY = xy; // okay
If properties not stated in every union member could hold any value if present:
type A = { x: number; } | Y;
const a: A = Math.random() < 0.5 ? { x: 123 } : val; // okay
To access properties of a union safely, they need to be declared in each member of the union. If a property might be missing or undefined
, specify it as optional or using the impossible never
type:
type A = { x: number, y?: never } | { x?: never, y: number };
let a: A;
a = val; // error!
Subsequently, a.x
will be either number
or undefined
:
a = Math.random() < 0.5 ? { x: 123 } : { y: 123 }; // okay
a.x?.toFixed(); // okay
The Use of the in
Operator in TypeScript
The limitation of accessing properties of unions unless defined in every member emphasizes that object types aren't precise. The compiler enforces type safety to avoid violations. Yet, despite such checks, some unsoundness exists in TypeScript, leveraging human considerations over full correctness.
An example of unsoundness lies in using the in
operator for narrowing unions which can lead to runtime errors:
if ("x" in a) {
a.x.toFixed(2); // no compiler error
} else {
a.y.toFixed(2); // no compiler error
}
By checking for the existence of an x
key in a
, the compiler narrows a
to {x: number}
allowing access without warning. Despite potential issues, the TypeScript team prioritizes usability over strict type enforcement.
Hence, the use of the in
operator before property access provides a workaround, assuming scenarios like XY
won't materialize frequently.
Link to Interactive Code Playground