In case you don't know what RighFminal typing
Typescript is a structural typing language, but it is possible to mimic the behavior of nominal typing language, very closely.
We start with the easiest method:
type AB = { a:number, b:number, x:never}
type BA = { a:number, b:number, y:never}
const ab:AB = {a:1, b:2}
const ba:BA = ab // error !!
It works
BUT THIS IS NOT GOOD ENOUGH !!
Today we are going to learn how to create a proper nominal type in Typescript
Let us review why the first method is not good enough.
First, it uses type assertion. Avoid type assertion if possible, personally I only assert type when I am creating wrappers.
Second, users can access the extra property: brand via static typing. In this case, our brand is ab.x
and ba.y
.
As a craftsman, we don't want that to happen
Solution
declare class AB_Class {
protected x?:never
}
declare class BA_Class {
protected x?:never
}
interface AB extends AB_Class{
a:number, b:number
}
interface BA extends BA_Class{
a:number, b:number
}
const ab:AB = {a:1, b:2}
const ba:BA = ab // error !!
The key is to use class and declare optional protected brands with never as type.
The brands name don't have to be the same name, you can assign different names.
- no type assertion
- users cannot access brand
now, you may ask, why protected
, why not private
?
This is because, take this example
declare class AB_Class {
private x?:never
}
after tsc
compilation, the output is
declare class AB_Class {
private x?
}
do you notice the difference?
Typescript strips away the type of private
and fails type assertion
so please stick with protected
Top comments (10)
Does not look like a proper solution:
not sure i understand this
let t2: Test = {}
is expected becausex
is supposed to be not existDo this instead:
Symbols are enumerable by default but
Object.keys
andfor-in
work only withstring
props.So it fixes all the above issues.
If you also want to prevent
Object.assign
copies, add this:Have you looked at zod brand?
github.com/colinhacks/zod#brand
I think zod brand is a very smart solution
but it has 2 problems
cat[BRAND]
cat[BRAND]
and value ofcat[BRAND]
mismatched1 is unsatisfying but overall is not a problem, because nobody is going to do that
2 is an easy fix, just add optional modifier, although it still mislead some to believe it may return
{ Cat: true }
overall I think zod solution is practically perfect
update:
I missed one point:
to assign Cat type to a variable, you either do
since we want to avoid type assertion if possible
then we are left with:
which is slightly unintuitive
ideally, we want
but this will error using zod method because it is missing symbol properties
In general TypeScript requires a runtime assertion in the absence of a compile time type assertion.
Playground
You can even do this to primitive types
Assertion Functions
Source
A type predicate can be used to a similar end.
In a sense the fact that
even works is no better than
avoid type assertion because it is prone to error
playground
You've missed the point.
Typescript's assignability has edge cases that may surprise some people, perhaps the most infamous one is that assignments are not subject to excess property checks.
So it could be argued:
Compare that to
playground
personally i avoid type predicate, because there is nothing to prevent what could goes wrong in the assertion, for example, i could change something in it(as shown in image) and it will still work, but it will give wrong result because the code is wrong
for excess property check problem, i believe it can be solved on type level, without relying on runtime assertion
it was a problem that haunted me, but it is not a concern to me anymore, so i believe i solved it at some point iirc, (or i just gave up, i will try to confirm it again)
update: this is the type level solution for excess property check, without relying on runtime assertion
playground
playground(simplified)