In TypeScript, object types are either "open" or "extendible" rather than "closed" or "exact". When you define a type like interface Foo {a: string}
, it means that a value must have a property named a
with a string value, but it does not restrict the presence of other properties. This allows you to create another interface, such as
interface Bar extends Foo {b: number}
, indicating that every instance of
Bar
is also considered an instance of
Foo
. Therefore, interface and class hierarchies represent type hierarchies in TypeScript. In your specific case, since
c
has a valid
TypeA
, there is no error.
Although some users desire exact types, as mentioned in the long-standing issue microsoft/TypeScript#12936, they are currently unsupported in TypeScript. Exact types would enforce strict adherance to a specific set of properties without allowing any additional ones. For now, developers can rely on excess property warnings for object literals to catch unexpected properties at compile time.
If you encounter a situation where you require precise types in TypeScript, there are workarounds suggested in the aforementioned issue. One solution involves using a generic helper function to compare a value against an "exactified" version of the desired type by mapping excess properties to the impossible never
type. Here's an example:
const exactly = <T extends object,>() =>
<U extends T & { [P in Exclude<keyof U, keyof T>]: never }>(
u: U): T => u;
const exactlyTypeA = exactly<TypeA>();
The exactlyTypeA()
function will only accept values that precisely match type A
, preventing any added properties from slipping through unnoticed. It's important to use this function early to avoid widening the type later on accidentally.
While these workarounds may not be foolproof and involve additional code, they offer a close alternative to true exact types in TypeScript.
Playground link