@@ -14,19 +14,11 @@ It offers three kinds of nominal types:
14
14
They are very useful when dealing with "rigid" code generators or other cases
15
15
where we would be forced to write tons of mappings just to content the type
16
16
checker.
17
- - ** Tags:** As you might imagine, * tags* allow us to "attach" multiple nominal
18
- types to a same variable. They are very useful to express things like:
19
- - * roles & capabilities* : some times interfaces & classes are not enough, we
20
- might need or want to encode many roles and/or capabilities at the same
21
- time for a single entity, and to use the type checker to enforce
22
- constraints based on that information.
23
- - * logical/mathematical properties* : each attached * tag* can be interpreted
24
- as, in some way, a property assertion (for example we could attach
25
- properties like * positive* , * odd* or * prime* to a number, at the same time).
26
-
27
- On top of these three kinds of nominal type, ` Nominal ` also offers
28
- [ taint tracking] ( https://en.wikipedia.org/wiki/Taint_checking ) capabilities with
29
- zero runtime overhead.
17
+ - ** Properties:** They are very useful to express things like logical and
18
+ mathematical properties.
19
+
20
+ While each type can only have either a * brand* or a * flavor* , we can easily
21
+ combine * brands* or * flavors* with * properties* .
30
22
31
23
## Install instructions
32
24
@@ -50,265 +42,161 @@ import { ... } from 'https://deno.land/x/nominal@[VERSION]/nominal/deno/index.ts
50
42
51
43
## Brands
52
44
53
- Pending explanation.
54
- ## Flavors
55
-
56
- Pending explanation.
57
-
58
- ## Tags
59
-
60
- ### Basic tags
61
-
62
- To define a new tagged type based on a previous type, we can do:
63
-
64
45
``` typescript
65
- import { WithTag } from ' @coderspirit/nominal'
46
+ import { WithBrand } from ' @coderspirit/nominal'
47
+
48
+ type Email = WithBrand <string , ' Email' >
49
+ type Username = WithBrand <string , ' Username' >
66
50
67
- type Prime = WithTag <number , ' Prime' >
51
+ const email
: Email = ' [email protected] ' as Email // Ok
52
+ const user: Username = ' admin' as Username // Ok
53
+ const text: string = email // OK
54
+ const anotherText: string = user // Ok
68
55
69
- const myPrime: Prime = 23 as Prime
56
+ const eMail
: Email = ' [email protected] ' // Error, as we don't have a cast here
57
+ const mail: Email = user // Error, as the brands don't match
70
58
```
71
59
72
60
#### ** Advice**
73
61
- Although we perform a "static cast" here, this should be done only when:
74
62
- the value is a literal (as in the example)
75
63
- in validation, sanitization and/or anticorruption layers.
76
64
- One way to protect against other developers "forging" the type is to use
77
- symbols instead of strings as "type tags" when defining the new nominal type.
78
-
79
- ### Combining tags
65
+ symbols instead of strings as property keys or property values when defining
66
+ the new nominal type.
80
67
81
- ` WithTag ` has been implemented in a way that allows us to easily compose many
82
- nominal types for a same value:
68
+ ## Flavors
83
69
84
70
``` typescript
85
- import { WithTag , WithTags } from ' @coderspirit/nominal'
71
+ import { WithFlavor } from ' @coderspirit/nominal'
86
72
87
- type Integer = WithTag <number , ' Integer' >
88
- type Even = WithTag <number , ' Even' >
89
- type Positive = WithTag <number , ' Positive' >
73
+ type Email = WithFlavor <string , ' Email' >
74
+ type Username = WithFlavor <string , ' Username' >
90
75
91
- // The first way is by adding "tags" to an already "tagged" type
92
- type EvenInteger = WithTag <Integer , ' Even' >
76
+ const email
: Email = ' [email protected] ' as Email // Ok
77
+ const user: Username = ' admin' as Username // Ok
78
+ const text: string = email // OK
79
+ const anotherText: string = user // Ok
93
80
94
- // The second way is by adding many "tags" at the same time
95
- type EvenPositive = WithTags < number , [ ' Even ' , ' Positive ' ]>
81
+ const eMail : Email = ' [email protected] ' // Ok, flavors are more flexible than brands
82
+ const mail : Email = user // Error, as the flavors don't match
96
83
```
97
84
98
- #### **Interesting properties**
99
- - ` WithTag ` and ` WithTags ` are additive, commutative and idempotent.
100
- - The previous point means that we don't have to worry about the order of
101
- composition, we won't suffer typing inconsistencies because of that.
102
-
103
- #### **Unused type tags can be preserved across function boundaries**
104
-
105
- This feature can be very useful when we need to verify many properties for the
106
- same value and we don't want to lose this information along the way as the value
107
- is passed from one function to another.
108
-
109
- ` ` ` typescript
110
- function throwIfNotEven<T extends number >(v : T ): WithTag <T , ' Even' > {
111
- if (v % 2 == 1 ) throw new Error (' Not Even!' )
112
- return v as WithTag <T , ' Even' >
113
- }
114
-
115
- function throwIfNotPositive<T extends number >(v : T ): WithTag <T , ' Positive' > {
116
- if (v <= 0 ) throw new Error (' Not positive!' )
117
- return v as WithTag <T , ' Positive' >
118
- }
119
-
120
- const v1 = 42
85
+ #### ** Advice**
86
+ - Although we perform a "static cast" here, this should be done only when:
87
+ - the value is a literal (as in the example)
88
+ - in validation, sanitization and/or anticorruption layers.
89
+ - One way to protect against other developers "forging" the type is to use
90
+ symbols instead of strings as property keys or property values when defining
91
+ the new nominal type.
121
92
122
- // typeof v2 === WithTag<number, 'Even'>
123
- const v2 = throwIfNotEven (v1 )
124
93
125
- // typeof v3 === WithTags<number, ['Even', 'Positive']>
126
- const v3 = throwIfNotPositive (v2 )
127
- ```
94
+ ## Properties
128
95
129
- ### Removing tags
96
+ ### Introduction
130
97
131
- If needed, we can easily remove * tags * from our types :
98
+ To define a new type with a property, we can do :
132
99
133
100
``` typescript
134
- import { WithTag , WithoutTag } from ' @coderspirit/nominal'
135
-
136
- type Email = WithTag <string , ' Email' >
137
-
138
- // NotEmail === string
139
- type NotEmail = WithoutTag <string , ' Email' >
140
-
141
- // NotAnEmailAnymore === string
142
- type NotAnEmailAnymore = WithoutTag <Email , ' Email' >
101
+ import { WithProperty } from ' @coderspirit/nominal'
102
+ type Even = WithProperty <number , ' Parity' , ' Even' >
103
+ const myEven: Even = 42 as Even
143
104
```
144
105
145
- The tags that we do not explicitly remove are preserved:
146
- ` ` ` typescript
147
- type FibonacciPrime = WithTags <number , [' Prime' , ' Fibonacci' ]>
148
- type Fibonacci = WithoutTag <FibonacciPrime , ' Prime' >
149
- ` ` `
106
+ If we want to use the properties as simple tags, we can omit the property value,
107
+ and it will implicitly default to ` true ` , although it's less flexible:
150
108
151
- WARNING: Notice that it's not a good idea to preserve all the previous tags for
152
- return types when the passed value is transformed. For example:
153
109
``` typescript
154
- function square<T extends number >(v : T ): WithoutTag <T , ' Prime' > {
155
- return v * v as WithoutTag <T , ' Prime' >
156
- }
157
-
158
- const myNumber: FibonacciPrime = 13 as FibonacciPrime
159
-
160
- // Notice that the return type's tag is wrong, as 169 is not a Fibonacci number
161
- // typeof mySquaredNumber === WithTag<number, 'Fibonacci'>
162
- // mySquaredNumber === 13*13 === 169
163
- const mySquaredNumber = square (myNumber )
164
-
110
+ import { WithProperty } from ' @coderspirit/nominal'
111
+ type Positive = WithProperty <number , ' Positive' >
112
+ const myPositive: Positive = 1 as Positive
165
113
```
166
114
167
- We can also remove many tags at once, or all of them:
168
- ``` typescript
169
- // Editor === WithTag<User, 'Editor'>
170
- type Editor = WithoutTags <
171
- WithTags < User , [ ' Editor ' , ' Moderator ' , ' Admin ' ]>,
172
- [ ' Moderator ' , ' Admin ' ]
173
- >
115
+ #### ** Advice **
116
+ - Although we perform a "static cast" here, this should be done only when:
117
+ - the value is a literal (as in the example)
118
+ - in validation, sanitization and/or anticorruption layers.
119
+ - One way to protect against other developers "forging" the type is to use
120
+ symbols instead of strings as property keys or property values when defining
121
+ the new nominal type.
174
122
175
- // NewNumber === number
176
- type NewNumber = WithoutTags <FibonacciPrime >
177
- ` ` `
123
+ #### ** Interesting properties**
124
+ - ` WithProperty ` is additive, commutative and idempotent.
125
+ - The previous point means that we don't have to worry about the order of
126
+ composition, we won't suffer typing inconsistencies because of that.
178
127
179
- ### Negating tags
128
+ ### Crazy-level strictness
180
129
181
- If needed , we can easily **negate** *tags* from our types. This is similar to
182
- removing them, but it allows us to reject some values with certain tags.
130
+ If we want , we can even define "property types", to ensure that we don't set
131
+ invalid values:
183
132
184
133
``` typescript
185
- import { WithTag , NegateTag } from ' @coderspirit/nominal'
186
-
187
- type Email = WithTag <string , ' Email' >
188
- type NegatedEmail = NegateTag <string , ' Email' >
189
- type NegatedEmail2 = NegateTag <Email , ' Email' >
190
-
191
- const email
: Email = ' [email protected] ' as Email
134
+ import { PropertyTypeDefinition , WithStrictProperty } from ' @coderspirit/nominal'
135
+ type Parity = PropertyTypeDefinition <' Parity' , ' Even' | ' Odd' >
192
136
193
- // The type checked accepts this
194
- const untypedEmail : string = email
137
+ // == WithProperty<number, 'Parity', 'Even'>
138
+ type Even = WithStrictProperty < number , Parity , ' Even ' >
195
139
196
- // The type checker will error with any of the following two lines
197
- const notEmail1: NegatedEmail = email // ERROR!
198
- const notEmail2: NegatedEmail2 = email // ERROR!
199
-
200
- // NotEmail & NotAnEmailAnymore are still compatible with string
201
- const notEmail3: NegatedEmail = ' not an email'
202
- const notEmail4: string = notEmail3 // This is OK :)
203
-
204
- const notEmail5: NegatedEmail2 = ' not an email anymore'
205
- const notEmail6: string = notEmail5 // This is also OK :)
140
+ // == never
141
+ type Wrong = WithStrictProperty <number , Parity , ' Seven' >
206
142
` ` `
207
143
208
- This can be a powerful building block to implement values tainting, although we
209
- already provide an out-of-box solution for that.
210
-
211
144
### Advanced use cases
212
145
213
- Now that we know how to [ add] ( #basic-tags ) , [ remove] ( #removing-tags ) , and
214
- [ negate] ( #negating-tags ) tags, let's see a fancy example:
215
-
216
- ``` typescript
217
- // By combining tags & tag negations we can define types that allow us to
218
- // express logical or mathematical properties in a consistent way.
219
-
220
- // Now we can use `Even` and `Odd` without fearing that they will be used at the
221
- // same time for the same variable.
222
- type Even <N extends number = number > = NegateTag <WithTag <N , ' Even' >, ' Odd' >
223
- type Odd <N extends number = number > = NegateTag <WithTag <N , ' Odd' >, ' Even' >
224
- type ChangeParity <N extends Even | Odd > = N extends Even ? Odd : Even
225
-
226
- type Positive <N extends number = number > = NegateTag <WithTag <N , ' Positive' >, ' Negative' >
227
- type Negative <N extends number = number > = NegateTag <WithTag <N , ' Negative' >, ' Positive' >
228
-
229
- // We preserve sign when the number is positive for obvious reasons, but we
230
- // cannot do the same for negative values (for example for the value -0.5).
231
- type PlusOneResult <N extends Even | Odd > = N extends Positive
232
- ? Positive <ChangeParity <N >> // Notice that we do not write Positive & ChangeParity<N>
233
- : ChangeParity <N >
234
-
235
- function <N extends Even | Odd >plusOne(v : N ): PlusOneResult <N > {
236
- return v + 1 as PlusOneResult <N >
237
- }
238
-
239
- const positiveEven: Positive <Even > = 42 as Positive <Even >
240
- const positiveOdd: Positive <Odd > = 3 as Positive <Odd >
241
-
242
- // typeof positiveEvenPlus1 == Positive<Odd>
243
- const positiveEvenPlus1 = plusOne (positiveEven )
244
-
245
- // typeof positiveOddPlus1 === Positive<Even>
246
- const positiveOddPlus1 = plusOne (positiveOdd )
247
-
146
+ #### **Properties can be preserved across function boundaries**
248
147
249
-
250
- ```
251
-
252
- ## Tainting
253
-
254
- ### Tainting values
255
-
256
- While using * brands* , * flavors* and * tags* is often enough, sometimes it can be
257
- handy to mark all the values coming from the "external world" as * tainted* (and
258
- therefore "dangerous"), independently of whether we took the time to assign them
259
- a specific nominal type or not.
260
-
261
- ` Nominal ` provides the types ` Tainted<T> ` and ` Untainted<T> ` , both of them
262
- operate recursively on ` T ` .
148
+ This feature can be very useful when we need to verify many properties for the
149
+ same value and we don't want to lose this information along the way as the value
150
+ is passed from one function to another.
263
151
264
152
` ` ` typescript
265
- import { Tainted , Untainted } from ' @coderspirit/nominal'
266
-
267
- interface LoginRequest {
268
- username: string
269
- password: string
153
+ function throwIfNotEven<T extends number >(v : T ): WithProperty <T , ' Parity' , ' Even' > {
154
+ if (v % 2 == 1 ) throw new Error (' Not Even!' )
155
+ return v as WithProperty <T , ' Even' >
270
156
}
271
157
272
- type TaintedLoginRequest = Tainted <LoginRequest >
273
- type UntaintedLoginRequest = Untainted <LoginRequest >
274
-
275
- function validateLoginRequest(req : TaintedLoginRequest ): UntaintedLoginRequest {
276
- // throw Error if `req` is invalid
277
- return req as UntaintedLoginRequest
158
+ function throwIfNotPositive<T extends number >(v : T ): WithProperty <T , ' Sign' , ' Positive' > {
159
+ if (v <= 0 ) throw new Error (' Not positive!' )
160
+ return v as WithProperty <T , ' Positive' >
278
161
}
279
162
280
- // This function accepts LoginRequest and UntaintedLoginRequest,
281
- // but not TaintedLoginRequest
282
- function login(req : UntaintedLoginRequest ): void {
283
- // Do stuff
284
- }
163
+ const v1 = 42
285
164
286
- // When req is tainted, we cannot pass req.password to this function, as all
287
- // req's fields are tainted as well.
288
- function doStuffWithPassword(password : Untainted <string >): void {
289
- // Do stuff
290
- }
165
+ // typeof v2 === WithProperty<number, 'Parity', 'Even'>
166
+ const v2 = throwIfNotEven (v1 )
167
+
168
+ // typeof v3 extends WithProperty<number, 'Parity', 'Even'>
169
+ // typeof v3 extends WithProperty<number, 'Sign', 'Positive'>
170
+ const v3 = throwIfNotPositive (v2 )
291
171
```
292
172
293
- While this specific use case is far from being exciting and quite simplistic,
294
- this idea can be applied to much more sensitive and convoluted scenarios.
173
+ #### Chosing what properties to preserve across function boundaries
295
174
296
- ### Generic tainting
175
+ In the previous example, we could add many properties because we were just
176
+ making assertions about the values. When we transform the passed values, we must
177
+ be more careful about what we preserve.
297
178
298
- While it's difficult to imagine how a value might be tainted in multiple ways,
299
- it's not unheard of. This could be useful when managing sensitive information,
300
- if we need/want to statically enforce that some data won't cross certain
301
- boundaries.
179
+ As a simple example of what we are telling here, we can see that adding ` 1 ` to a
180
+ numeric variable would flip its parity, so in that case we wouldn't want to keep
181
+ that property on the return value.
302
182
303
183
``` typescript
304
- import { GenericTainted , GenericUntainted } from ' @coderspirit/nominal'
305
-
306
- type BlueTaintedNumber = GenericTainted <number , ' Blue' >
307
- type RedTaintedNumber = GenericTainted <number , ' Red' >
308
-
309
- // Double-tainted type!
310
- type BlueRedTaintedNumber = GenericTainted <BlueTaintedNumber , ' Red' >
184
+ type Even <N extends number = number > = WithProperty <N , ' Parity' , ' Even' >
185
+ type Odd <N extends number = number > = WithProperty <N , ' Parity' , ' Odd' >
186
+
187
+ // 1. 'Parity' is overwritten (when available)
188
+ // 2. 'Sign' is kept only if it's positive
189
+ // 3. We discard all other properties because they might stop being true
190
+ type PlusOneResult <N > = KeepProperties <
191
+ N extends Even
192
+ ? KeepPropertyIfValueMatches <Odd <N >, ' Sign' , ' Positive' >
193
+ : N extends Odd
194
+ ? KeepPropertyIfValueMatches <Even <N >, ' Sign' , ' Positive' >
195
+ : KeepPropertyIfValueMatches <N , ' Sign' , ' Positive' >,
196
+ ' Sign' | ' Parity'
197
+ >
311
198
312
- // We removed again the 'Blue' taint
313
- type OnlyBlueTaintedNumber = GenericUntainted <BlueRedTaintedNumber , ' Red' >
199
+ function plusOne<N extends number >(v : N ): PlusOneResult <N > {
200
+ return v + 1 as PlusOneResult <N >
201
+ }
314
202
```
0 commit comments