1/**
2 * The `Brand` module adds compile-time names to ordinary TypeScript values so
3 * structurally identical values cannot be mixed accidentally. A branded value
4 * has the same runtime representation as its unbranded value; the extra
5 * information lives in the type system unless you choose a validating
6 * constructor.
7 *
8 * @since 2.0.0
9 */
10import * as Arr from "./Array.ts"
11import * as Option from "./Option.ts"
12import * as Result from "./Result.ts"
13import type * as Schema from "./Schema.ts"
14import * as SchemaAST from "./SchemaAST.ts"
15import * as SchemaIssue from "./SchemaIssue.ts"
16import type * as Types from "./Types.ts"
17
18const TypeId = "~effect/Brand"
19
20/**
21 * A generic interface that defines a branded type.
22 *
23 * **When to use**
24 *
25 * Use to define a branded type such as `number & Brand<"Positive">` when
26 * TypeScript should keep structurally identical values separate without
27 * changing their runtime value.
28 *
29 * @see {@link Branded} for applying a brand key to a base type
30 * @see {@link Constructor} for validating or constructing branded values
31 *
32 * @category models
33 * @since 2.0.0
34 */
35export interface Brand<in out Keys extends string> {
36 readonly [TypeId]: {
37 readonly [K in Keys]: Keys
38 }
39}
40
41/**
42 * A constructor for a branded type that provides validation and safe
43 * construction methods.
44 *
45 * **When to use**
46 *
47 * Use as the shared callable interface for branded values when an API accepts
48 * or returns a brand constructor and callers need throwing, `Option`, `Result`,
49 * or type-guard validation forms.
50 *
51 * @see {@link nominal} for a constructor without runtime validation
52 * @see {@link make} for creating a constructor from a validation predicate
53 * @see {@link check} for creating a constructor from schema checks
54 * @see {@link all} for combining brand constructors
55 *
56 * @category models
57 * @since 2.0.0
58 */
59export interface Constructor<in out B extends Brand<any>> {
60 /**
61 * Constructs a branded type from a value of type `Unbranded<B>`, throwing an
62 * error if the provided value is not valid.
63 */
64 (unbranded: Brand.Unbranded<B>): B
65 /**
66 * Constructs a branded type from a value of type `Unbranded<B>`, returning
67 * `Some<B>` if the provided value is valid, `None` otherwise.
68 */
69 option(unbranded: Brand.Unbranded<B>): Option.Option<B>
70 /**
71 * Constructs a branded type from a value of type `Unbranded<B>`, returning
72 * `Success<B>` if the provided value is valid, `Failure<BrandError>`
73 * otherwise.
74 */
75 result(unbranded: Brand.Unbranded<B>): Result.Result<B, BrandError>
76 /**
77 * Attempts to refine the provided value of type `Unbranded<B>`, returning
78 * `true` if the provided value is a valid branded type, `false` otherwise.
79 */
80 is(unbranded: Brand.Unbranded<B>): unbranded is Brand.Unbranded<B> & B
81
82 /**
83 * The checks that are applied to the branded type.
84 *
85 * @internal
86 */
87 checks?: readonly [SchemaAST.Check<Brand.Unbranded<B>>, ...Array<SchemaAST.Check<Brand.Unbranded<B>>>] | undefined
88}
89
90/**
91 * Error returned when a branded type is constructed from an invalid value.
92 *
93 * **Details**
94 *
95 * The error wraps a `SchemaIssue.Issue`, renders `message` with the default
96 * schema issue formatter, and formats as `BrandError(<message>)`.
97 *
98 * **Gotchas**
99 *
100 * `BrandError` is an error-like model with `_tag`, `name`, `message`, and
101 * `toString`; it does not extend JavaScript `Error`.
102 *
103 * @category errors
104 * @since 4.0.0
105 */
106export class BrandError {
107 constructor(issue: SchemaIssue.Issue) {
108 this.issue = issue
109 }
110 /**
111 * Discriminant used to identify brand construction failures.
112 *
113 * @since 4.0.0
114 */
115 readonly _tag = "BrandError"
116 /**
117 * Error name used by tools that inspect JavaScript error-like objects.
118 *
119 * @since 4.0.0
120 */
121 readonly name: string = "BrandError"
122 /**
123 * Schema issue describing why brand validation failed.
124 *
125 * @since 4.0.0
126 */
127 readonly issue: SchemaIssue.Issue
128 /**
129 * Human-readable rendering of the validation issue.
130 *
131 * @since 4.0.0
132 */
133 get message() {
134 return SchemaIssue.defaultFormatter(this.issue)
135 }
136 /**
137 * Formats the brand error together with its validation message.
138 *
139 * @since 4.0.0
140 */
141 toString() {
142 return `BrandError(${this.message})`
143 }
144}
145
146/**
147 * Namespace containing type-level helpers for working with branded types and
148 * brand constructors.
149 *
150 * @since 2.0.0
151 */
152export declare namespace Brand {
153 /**
154 * A utility type to extract a branded type from a `Constructor`.
155 *
156 * @category utility types
157 * @since 2.0.0
158 */
159 export type FromConstructor<C> = C extends Constructor<infer B> ? B : never
160
161 /**
162 * A utility type to extract the unbranded value type from a brand.
163 *
164 * @category utility types
165 * @since 2.0.0
166 */
167 export type Unbranded<B extends Brand<any>> = B extends infer U & Brands<B> ? U : B
168
169 /**
170 * A utility type to extract the keys of a branded type.
171 *
172 * @category utility types
173 * @since 4.0.0
174 */
175 export type Keys<B extends Brand<any>> = keyof B[typeof TypeId]
176
177 /**
178 * A utility type to extract the brands from a branded type.
179 *
180 * @category utility types
181 * @since 2.0.0
182 */
183 export type Brands<B extends Brand<any>> = Types.UnionToIntersection<
184 { [K in Keys<B>]: K extends string ? Brand<K> : never }[Keys<B>]
185 >
186
187 /**
188 * A utility type that checks that all brands have the same base type.
189 *
190 * @category utility types
191 * @since 2.0.0
192 */
193 export type EnsureCommonBase<
194 Brands extends readonly [Constructor<any>, ...Array<Constructor<any>>]
195 > = {
196 [B in keyof Brands]: Brand.Unbranded<Brand.FromConstructor<Brands[0]>> extends
197 Brand.Unbranded<Brand.FromConstructor<Brands[B]>>
198 ? Brand.Unbranded<Brand.FromConstructor<Brands[B]>> extends Brand.Unbranded<Brand.FromConstructor<Brands[0]>>
199 ? Brands[B]
200 : Brands[B]
201 : "ERROR: All brands should have the same base type"
202 }
203}
204
205/**
206 * A type alias for creating branded types more concisely.
207 *
208 * @category utility types
209 * @since 2.0.0
210 */
211export type Branded<A, Key extends string> = A & Brand<Key>
212
213/**
214 * Returns a `Constructor` that **does not apply any runtime checks** and just
215 * returns the provided value.
216 *
217 * **When to use**
218 *
219 * Use to create nominal types that allow distinguishing between two values
220 * of the same type but with different meanings.
221 *
222 * @see {@link make} for constructing branded values with validation.
223 * @see {@link check} for constructing branded values from schema checks.
224 *
225 * @category constructors
226 * @since 2.0.0
227 */
228export function nominal<A extends Brand<any>>(): Constructor<A> {
229 return Object.assign((input: Brand.Unbranded<A>) => input as A, {
230 option: (input: Brand.Unbranded<A>) => Option.some(input as A),
231 result: (input: Brand.Unbranded<A>) => Result.succeed(input as A),
232 is: (_: Brand.Unbranded<A>): _ is Brand.Unbranded<A> & A => true
233 })
234}
235
236/**
237 * Returns a `Constructor` that can construct a branded type from an unbranded
238 * value using the provided `filter` predicate as validation of the input data.
239 *
240 * **When to use**
241 *
242 * Use when you want validation while constructing the branded type.
243 *
244 * @see {@link nominal} for a brand constructor that performs no validation.
245 *
246 * @category constructors
247 * @since 4.0.0
248 */
249export function make<A extends Brand<any>>(
250 filter: (unbranded: Brand.Unbranded<A>) => Schema.FilterOutput
251): Constructor<A> {
252 return check(SchemaAST.makeFilter(filter))
253}
254
255/**
256 * Creates a branded type `Constructor` from one or more schema checks.
257 *
258 * **When to use**
259 *
260 * Use when you need a branded type constructor that performs runtime validation
261 * via schema checks.
262 *
263 * **Details**
264 *
265 * Calling the returned constructor validates the unbranded value and throws on
266 * failure. Use the returned `option`, `result`, or `is` methods for
267 * non-throwing validation.
268 *
269 * @see {@link nominal} for a brand constructor without runtime validation
270 * @see {@link all} for combining multiple brand constructors
271 * @category constructors
272 * @since 4.0.0
273 */
274export function check<A extends Brand<any>>(
275 ...checks: readonly [
276 SchemaAST.Check<Brand.Unbranded<A>>,
277 ...Array<SchemaAST.Check<Brand.Unbranded<A>>>
278 ]
279): Constructor<A> {
280 const result = (input: Brand.Unbranded<A>): Result.Result<A, BrandError> => {
281 return Result.mapError(SchemaAST.runChecks(checks, input), (issue) => new BrandError(issue)) as any
282 }
283 return Object.assign((input: Brand.Unbranded<A>) => Result.getOrThrow(result(input)), {
284 option: (input: Brand.Unbranded<A>) => Option.getSuccess(result(input)),
285 result,
286 is: (input: Brand.Unbranded<A>): input is Brand.Unbranded<A> & A => Result.isSuccess(result(input)),
287 checks
288 })
289}
290
291/**
292 * Combines one or more brand constructors to form a single branded type.
293 *
294 * **When to use**
295 *
296 * Use to require an input to satisfy every runtime check collected by the
297 * provided brand constructors.
298 *
299 * **Details**
300 *
301 * If the provided constructors contain runtime checks, the combined
302 * constructor succeeds only when all checks pass. If no runtime checks are
303 * present, it behaves as a nominal constructor.
304 *
305 * @category combining
306 * @since 2.0.0
307 */
308export function all<Brands extends readonly [Constructor<any>, ...Array<Constructor<any>>]>(
309 ...brands: Brand.EnsureCommonBase<Brands>
310): Constructor<
311 Types.UnionToIntersection<{ [B in keyof Brands]: Brand.FromConstructor<Brands[B]> }[number]> extends
312 infer X extends Brand<any> ? X : Brand<any>
313> {
314 const checks = brands.flatMap((brand) => brand.checks ?? [])
315 return Arr.isArrayNonEmpty(checks) ?
316 check(...checks) :
317 nominal()
318}