-
Notifications
You must be signed in to change notification settings - Fork 7
/
newtype.ts
233 lines (221 loc) · 5.94 KB
/
newtype.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
/**
* Newtype presents a type level "rebranding" of an existing type.
*
* It's basic purpose is to create a branded type from an existing
* type. This is much like using TypeScript type aliases, ie.
* `type MyNumber = number`. However, Newtype will prevent the
* existing type from being used where the Branded type is
* specified.
*
* @module Newtype
* @since 2.0.0
*/
import type { Combinable } from "./combinable.ts";
import type { Comparable } from "./comparable.ts";
import type { Initializable } from "./initializable.ts";
import type { Iso, Prism } from "./optic.ts";
import type { Option } from "./option.ts";
import type { Predicate } from "./predicate.ts";
import type { Sortable } from "./sortable.ts";
import { fromPredicate } from "./option.ts";
import { iso as _iso, prism as _prism } from "./optic.ts";
import { identity, unsafeCoerce } from "./fn.ts";
const BrandSymbol = Symbol("Brand");
const ValueSymbol = Symbol("Value");
/**
* Create a branded type from an existing type. The branded
* type can be used anywhere the existing type can, but the
* existing type cannot be used where the branded one can.
*
* It is important to note that while a Newtype looks like
* a struct, its actual representation is that of the
* inner type.
*
* @example
* ```ts
* import type { Newtype } from "./newtype.ts";
*
* type Integer = Newtype<'Integer', number>;
*
* const int = 1 as Integer;
* const num = 1 as number;
*
* declare function addOne(n: Integer): number;
*
* addOne(int); // This is ok!
* // addOne(num); // This is not
* ```
*
* @since 2.0.0
*/
export type Newtype<B, A> = A & {
readonly [ValueSymbol]: A;
readonly [BrandSymbol]: B;
};
/**
* A type alias for Newtype<any, any> that is useful when constructing
* Newtype related runtime instances.
*
* @since 2.0.0
*/
export type AnyNewtype = Newtype<unknown, unknown>;
/**
* Extracts the inner type value from a Newtype.
*
* @example
* ```ts
* import type { Newtype, ToValue } from "./newtype.ts";
*
* type Integer = Newtype<'Integer', number>;
*
* type InnerInteger = ToValue<Integer>; // number
* ```
*
* @since 2.0.0
*/
export type ToValue<T extends AnyNewtype> = T extends Newtype<infer _, infer A>
? A
: never;
/**
* Retype an existing Comparable from an inner type to a Newtype.
*
* @example
* ```ts
* import { Newtype, getComparable } from "./newtype.ts";
* import * as N from "./number.ts";
*
* type Integer = Newtype<'Integer', number>;
*
* const eqInteger = getComparable<Integer>(N.ComparableNumber);
* ```
*
* @since 2.0.0
*/
export function getComparable<T extends AnyNewtype>(
eq: Comparable<ToValue<T>>,
): Comparable<T> {
return eq as unknown as Comparable<T>;
}
/**
* Retype an existing Sortable from an inner type to a Newtype.
*
* @example
* ```ts
* import { Newtype, getSortable } from "./newtype.ts";
* import * as N from "./number.ts";
*
* type Integer = Newtype<'Integer', number>;
*
* const ordInteger = getSortable<Integer>(N.SortableNumber);
* ```
*
* @since 2.0.0
*/
export function getSortable<T extends AnyNewtype>(
ord: Sortable<ToValue<T>>,
): Sortable<T> {
return ord as unknown as Sortable<T>;
}
/**
* Retype an existing Combinable from an inner type to a Newtype.
*
* @example
* ```ts
* import { Newtype, getCombinable } from "./newtype.ts";
* import * as N from "./number.ts";
*
* type Integer = Newtype<'Integer', number>;
*
* const monoidInteger = getCombinable<Integer>(N.InitializableNumberSum);
* ```
*
* @since 2.0.0
*/
export function getCombinable<T extends AnyNewtype>(
combinable: Combinable<ToValue<T>>,
): Combinable<T> {
return combinable as unknown as Combinable<T>;
}
/**
* Retype an existing Initializable from an inner type to a Newtype.
*
* @example
* ```ts
* import { Newtype, getInitializable } from "./newtype.ts";
* import * as N from "./number.ts";
*
* type Integer = Newtype<'Integer', number>;
*
* const monoidInteger = getInitializable<Integer>(N.InitializableNumberSum);
* ```
*
* @since 2.0.0
*/
export function getInitializable<T extends AnyNewtype>(
initializable: Initializable<ToValue<T>>,
): Initializable<T> {
return initializable as unknown as Initializable<T>;
}
// deno-lint-ignore no-explicit-any
const _anyIso: Iso<any, any> = _iso(unsafeCoerce, unsafeCoerce);
/**
* If the Newtype and its underlying value are referentially transparent
* (meaning they can always be swapped) then you can create an instance of Iso
* for the Newtype for mapping back and forth.
*
* @example
* ```ts
* import * as N from "./newtype.ts";
*
* type Real = N.Newtype<"Real", number>;
*
* const isoReal = N.iso<Real>();
*
* const result1: Real = isoReal.view(1); // Turn number into Real
* const result2: number = isoReal.review(result1); // Turn Real into number
* ```
*
* @since 2.0.0
*/
export function iso<T extends AnyNewtype>(): Iso<ToValue<T>, T> {
return _anyIso;
}
/**
* If the Newtype and its underlying value are not referentially transparent
* (meaning they can always be swapped) then you can create an instance of Prism
* for the Newtype in order to optionally map into the Newtype given some
* Predicate.
*
* @example
* ```ts
* import type { Option } from "./option.ts";
*
* import * as N from "./newtype.ts";
* import * as O from "./option.ts";
* import { pipe } from "./fn.ts";
*
* type Integer = N.Newtype<"Integer", number>;
*
* const prismInteger = N.prism<Integer>(Number.isInteger);
*
* // Turn number into an Option<Integer>
* const result1: Option<Integer> = prismInteger.view(1);
*
* // Turn an Option<Integer> into a number or fall back to 0 if Option is None
* const result2: number = pipe(
* result1,
* O.map(prismInteger.review),
* O.getOrElse(() => 0)
* );
* ```
*
* @since 2.0.0
*/
export function prism<T extends AnyNewtype>(
predicate: Predicate<ToValue<T>>,
): Prism<ToValue<T>, T> {
return _prism<ToValue<T>, T>(
fromPredicate(predicate) as (s: ToValue<T>) => Option<T>,
identity as (a: T) => ToValue<T>,
);
}