Skip to content

Commit 7964301

Browse files
authored
Merge pull request #32 from Coder-Spirit/the-big-refactor
feat!: introduce brands & flavors
2 parents 7f3b2d5 + 624cd32 commit 7964301

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1438
-1930
lines changed

README.md

Lines changed: 106 additions & 218 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,11 @@ It offers three kinds of nominal types:
1414
They are very useful when dealing with "rigid" code generators or other cases
1515
where we would be forced to write tons of mappings just to content the type
1616
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*.
3022

3123
## Install instructions
3224

@@ -50,265 +42,161 @@ import { ... } from 'https://deno.land/x/nominal@[VERSION]/nominal/deno/index.ts
5042

5143
## Brands
5244

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-
6445
```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'>
6650

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
6855

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
7058
```
7159

7260
#### **Advice**
7361
- Although we perform a "static cast" here, this should be done only when:
7462
- the value is a literal (as in the example)
7563
- in validation, sanitization and/or anticorruption layers.
7664
- 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.
8067

81-
`WithTag` has been implemented in a way that allows us to easily compose many
82-
nominal types for a same value:
68+
## Flavors
8369

8470
```typescript
85-
import { WithTag, WithTags } from '@coderspirit/nominal'
71+
import { WithFlavor } from '@coderspirit/nominal'
8672

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'>
9075

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
9380

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
9683
```
9784

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.
12192

122-
// typeof v2 === WithTag<number, 'Even'>
123-
const v2 = throwIfNotEven(v1)
12493

125-
// typeof v3 === WithTags<number, ['Even', 'Positive']>
126-
const v3 = throwIfNotPositive(v2)
127-
```
94+
## Properties
12895

129-
### Removing tags
96+
### Introduction
13097

131-
If needed, we can easily remove *tags* from our types:
98+
To define a new type with a property, we can do:
13299

133100
```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
143104
```
144105

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:
150108

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:
153109
```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
165113
```
166114

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.
174122

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.
178127

179-
### Negating tags
128+
### Crazy-level strictness
180129

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:
183132

184133
```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'>
192136

193-
// The type checked accepts this
194-
const untypedEmail: string = email
137+
// == WithProperty<number, 'Parity', 'Even'>
138+
type Even = WithStrictProperty<number, Parity, 'Even'>
195139

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'>
206142
```
207143
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-
211144
### Advanced use cases
212145
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**
248147
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.
263151
264152
```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'>
270156
}
271157

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'>
278161
}
279162

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
285164

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)
291171
```
292172

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
295174

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.
297178

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.
302182

303183
```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+
>
311198

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+
}
314202
```

0 commit comments

Comments
 (0)