import { Err, Result } from "./result"; import { unionIssue } from "./issue"; import { RuntimeType, runtimeTypeOf } from "./issues/shared"; import { asKind } from "./as-kind"; import type { Kind, TypedKind } from "./kind"; export type Projection = | { readonly kind: "none" } | { readonly kind: "structural", readonly value: T } | { readonly kind: "opaque", readonly value: T }; export type ProjectionKind = Projection["kind"]; export abstract class Type { declare readonly _type: T; abstract check(val: any): Result; abstract sliceResult(val: any): Result; assert(val: any): T { return assert(this.check(val)); } slice(val: any): T { return assert(this.sliceResult(val)); } /** * Use as a type guard. * * @example * const FooChecker: t.Type = ... * if (FooChecker.guard(val)) { * // in this scope, val has type T * } */ guard(val: any): val is T { const result = this.sliceResult(val); return !(result instanceof Err) } /** * Typescript helper that enforces the correct declaration of a value with no * overhead. * * @example * const User = t.subtype({ name: t.str, email: t.str }) * const myUser = User.({ dog: 'cow' }) // Typescript compile error */ literal(val: T): T { return val; } /* * Type algebra * ----------------------------------------------------------------------------------------------- */ and(r: Type): Type { return new Intersection([ asKind(this), asKind(r), ]); } or(r: Type): Either { return new Either(asKind(this), asKind(r)); } /* * Custom validators * ----------------------------------------------------------------------------------------------- */ validate(desc: string, fn: Validator): Type { return this.and(new Validation(desc, fn)); } /* * Comments, for nice TypeScript exporting * ----------------------------------------------------------------------------------------------- */ comment(comment: string): Comment { return new Comment(comment, asKind(this)); } } export abstract class TypeImpl extends Type { protected abstract merge(type: TypedKind): TypedKind | undefined; protected abstract project(val: any): Projection; protected projectionOf(type: TypeImpl, val: any): Projection { return type.project(val); } and(r: Type): Type { const rightKind = asKind(r); return intersect(asKind(this), rightKind, (l, r) => { const left = l as TypeImpl; const right = r as TypeImpl; return left.merge(r) || right.merge(l); }); } } type Merge = (l: Kind, r: Kind) => Kind | undefined; function intersect(l: TypedKind, r: TypedKind, merge: Merge): TypedKind { return normalizeIntersection([ ...flattenIntersection(l), ...flattenIntersection(r), ], merge) as TypedKind; } function flattenIntersection(type: Kind): Kind[] { if(type instanceof Intersection) { let operands: Kind[] = []; for(const operand of type.operands) { operands = operands.concat(flattenIntersection(operand)); } return operands; } return [ type ]; } function normalizeIntersection(operands: Kind[], merge: Merge): Kind { for(let l = 0; l < operands.length; l++) { for(let r = l + 1; r < operands.length; r++) { const merged = merge(operands[l], operands[r]); if(!merged) continue; const normalized = operands.slice(); normalized.splice(r, 1); normalized.splice(l, 1, ...flattenIntersection(merged)); return normalizeIntersection(normalized, merge); } } if(operands.length === 1) return operands[0]; return new Intersection(operands); } function assert(result: Result): T { if(result instanceof Err) throw result.toError(); return result; } /* * Type operators * ------------------------------------------------------------------------------------------------- * * These are types that operate on types, and are exposed in a fluent-style API on all Type objects. * To avoid circular dependency issues with module resolution, we keep them in this file, because * the Type class needs access to them in able to return them, but they also need access to the Type * class in order to extend it (since they themselves are Types). */ export abstract class UnmergeableType extends TypeImpl { sliceResult(val: any): Result { const checked = this.check(val); if(checked instanceof Err) return checked; const projection = this.project(val); if(projection.kind === "none") return checked; return projection.value; } protected merge(_: TypedKind): TypedKind | undefined { return undefined; } } export abstract class ConstraintType extends UnmergeableType { protected project(_: any): Projection { return { kind: "none" }; } } export abstract class OpaqueType extends UnmergeableType { protected project(val: any): Projection { return { kind: "opaque", value: val as T }; } } /* * ### Comment * * A type that delegates to the given type, but adds a comment when converted to TypeScript */ export class Comment extends TypeImpl { readonly wrapped: TypedKind; constructor(readonly commentStr: string, wrapped: Type) { super(); this.wrapped = asKind(wrapped); } check(val: any): Result { return this.wrapped.check(val); } sliceResult(val: any): Result { return this.wrapped.sliceResult(val); } protected merge(type: TypedKind): TypedKind { return new Comment( this.commentStr, this.wrapped.and(type), ); } protected project(val: any): Projection { return this.projectionOf(this.wrapped, val); } } /* * ### Validation * * A type that runs validation functions and errors if they return false, or passes if they return * true. * * This class is the sole operator that doesn't extend CustomCommutativeAndType, since there's no * way to automatically intersect validation types with anything else -- they don't wrap a real * type, they wrap arbitrary functions. */ export type Validator = (val: T) => boolean; export type ValidationIssue = { readonly kind: "validation"; readonly description: string; readonly threw: boolean; readonly subject: RuntimeType; }; export class Validation extends ConstraintType { readonly desc: string; readonly validator: Validator; constructor(desc: string, fn: Validator) { super(); this.desc = desc; this.validator = fn; } check(val: any): Result { try { if(this.validator(val)) return val; } catch { return new Err({ kind: "validation", description: this.desc, threw: true, subject: runtimeTypeOf(val), }); } return new Err({ kind: "validation", description: this.desc, threw: false, subject: runtimeTypeOf(val), }); } } /* * Either * ------------------------------------------------------------------------------------------------- * * A union type. Extends the KeyTrackingType since it may need to track its children's keys, if the * children are themselves doing key tracking. */ export class Either extends TypeImpl { readonly l: TypedKind; readonly r: TypedKind; constructor(l: Type, r: Type) { super(); this.l = asKind(l); this.r = asKind(r); } check(val: any): Result { const l = this.l.check(val); if(!(l instanceof Err)) return l; const r = this.r.check(val); if(!(r instanceof Err)) return r; return new Err(unionIssue([ l.issue, r.issue ], runtimeTypeOf(val))); } /* * Delegate slicing to each branch so a successful branch validates and projects captured values * in one pass instead of being rechecked by project(). */ sliceResult(val: any): Result { const l = this.l.sliceResult(val); if(!(l instanceof Err)) return l; const r = this.r.sliceResult(val); if(!(r instanceof Err)) return r; return new Err(unionIssue([ l.issue, r.issue ], runtimeTypeOf(val))); } protected merge(type: TypedKind): TypedKind<(L|R) & Incoming> { return new Either( this.l.and(type), this.r.and(type), ); } protected project(val: any): Projection { const l = this.l.check(val); if(!(l instanceof Err)) return this.projectionOf(this.l, val); return this.projectionOf(this.r, val); } } /* * Intersect * ------------------------------------------------------------------------------------------------- */ export class Intersection extends TypeImpl { constructor(readonly operands: ReadonlyArray) { super(); } check(val: any): Result { for(const operand of this.operands) { const result = operand.check(val); if(result instanceof Err) return result; } return val as T; } sliceResult(val: any): Result { const checked = this.check(val); if(checked instanceof Err) return checked; const projection = this.project(val); if(projection.kind === "none") return checked; return projection.value; } protected merge(type: TypedKind): TypedKind { return asKind(this.and(type)); } protected project(val: any): Projection { const projections = this.operands.map(type => this.projectionOf(type, val)); if(projections.some(projection => projection.kind === "opaque")) { return { kind: "opaque", value: val as T }; } const structural = projections.filter( (projection): projection is Extract, { kind: "structural" }> => { return projection.kind === "structural"; } ); if(structural.length === 0) return { kind: "none" }; if(structural.length === 1) return structural[0]; return { kind: "opaque", value: val as T }; } }