Java’s type system is nominal, i.e.
class A {
String x;
}
class B {
String x;
}
are different types, we can’t assign a B
to an A
variable.
TypeScript’s type system is structural, i.e.
class A {
x: string;
}
class B {
x: string;
}
are equal types, that is let a: A = new B();
is allowed.
Anything that has a string
property named x
qualifies as an A
.
Comparison
Nominal types are safer as they prevent accidental type equivalence. Structural types are more flexible, e.g. new new super-types can be created without adapting the existing subtypes.
Mimicking nominal types in TypeScript
Add a literal field to distinguish strucurally equal types:
type ValidatedString = string & { __brand: "validated" };
const print = (s: ValidatedString) => {
console.log(s);
};
print('Plain string') // compile error
print('Plain string' as ValidatedString)
We tell TypeScript, which nominal variant the object is by casting, i.e. we need to be careful at the “entrypoints” into nominal types.
References:
- GitHub issue discussing nominal types for TypeScript (~500 comments)
- Mimicking nominal types in TypeScript using brands
- Blog post by Michal Zalecki comparing different approaches to nominal types in TypeScript