import { Err, Result } from "../result"; import { Type, CustomCommutativeAndType, Either, DefaultIntersect, Comment } from "../type"; import { GetType } from "../get-type"; /* * MissingKey is a marker type that indicates that the key in a struct that holds an MissingKey is * optional. However, if the key is present, the value must typecheck T; it can't be left undefined. * This can be useful for verification but it's not equivalent to TypeScript's optional fields, * which can be left undefined even when present. */ export class MissingKey> { constructor(readonly type: T) {} } /* * OptionalKey mimics TypeScript's ?: optional field syntax in types/interfaces: it allows the key * to be missing, or set to undefined. */ export class OptionalKey> { constructor(readonly type: T) {} } type WrapperOrType = T extends MissingKey ? Inner : T extends OptionalKey ? Inner : T; type RawDict = { [key: string]: V; }; abstract class MergeableType extends CustomCommutativeAndType { and(type: Type): Type { if(type instanceof MergeableType) { // @ts-ignore return new MergeIntersect(this, type); } else if(type instanceof Either) { return new Either( this.and(type.l), this.and(type.r), ); } else if(type instanceof DefaultIntersect) { return new DefaultIntersect( this.and(type.l), this.and(type.r), ); } else if(type instanceof Comment) { return new Comment(type.commentStr, this.and(type.wrapped)); } return super.and(type); } } // Dicts and structs merge together in TypeScript, so we put both in the struct file export class Dict extends MergeableType> { readonly valueType: Type; constructor(v: Type, readonly namedKey: string = "key") { super(); this.valueType = v; } keyName(key: string): Dict { return new Dict(this.valueType, key); } check(val: any): Result> { const err = basicDictErrs(val); if(err) return err; for(const prop in val) { const result = this.valueType.check(val[prop]); if(result instanceof Err) return new Err(`[${prop}]: ${result.message}`); } return val as Result>; } sliceResult(val: any): Result> { const err = basicDictErrs(val); if(err) return err; const result: { [key: string]: any } = {}; for(const prop in val) { const sliced = this.valueType.sliceResult(val[prop]); if(sliced instanceof Err) return new Err(`[${prop}]: ${result.message}`); result[prop] = sliced; } return result as Result>; } } function basicDictErrs(val: any): Err | null { if(typeof val !== 'object') return new Err(`${val} is not an object`); if(Array.isArray(val)) return new Err(`${val} is an array`); if(val === null) return new Err(`${val} is null`); return null; } export function dict(v: Type): Dict { return new Dict(v); } export type FieldDef = Type | MissingKey | OptionalKey; export type TypeStruct = { [key: string]: FieldDef }; // see this blog post for an explanation of this type shenanigans // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html // Returns a type like `"foo" | "bar"` for all the optional keys in a typestruct type OptionalPropertyNames = { [K in keyof T]: T[K] extends MissingKey ? K : T[K] extends OptionalKey ? K : never; }[keyof T]; // Unwraps Type and OptionalKey> to T for all keys in a typestruct type UnwrapTypes = { [K in keyof T]: GetType>; }; export type UnwrappedTypeStruct = /* required props */ Pick, Exclude>> & /* optional props */ Partial, OptionalPropertyNames>>; export type TypeStructFor = { [K in keyof T]: Type; }; export type StructFor = Struct>; export function keyType(box: MissingKey> | OptionalKey> | Type): Type { if(box instanceof MissingKey || box instanceof OptionalKey) { return box.type; } return box; } export function allowsMissing>(box: MissingKey | T): box is MissingKey { return (box instanceof MissingKey); } export function allowsOptional>(box: MissingKey | T): box is OptionalKey { return (box instanceof OptionalKey); } export class Struct extends MergeableType> { readonly definition: T; readonly exact: boolean; constructor(definition: T, exact: boolean) { super(); this.definition = definition; this.exact = exact; } check(val: any): Result> { const typeErr = this.checkType(val); if(typeErr) return typeErr; const errs = this.checkFields(val, (t, val) => t.check(val)); if(errs.length === 0) return val as UnwrappedTypeStruct; return new Err(`${val} failed the following checks:\n${errs.join('\n')}`); } sliceResult(val: any): Result> { const typeErr = this.checkType(val); if(typeErr) return typeErr; const sliced: { [key: string]: any } = {}; const errs = this.checkFields(val, (t, val) => t.sliceResult(val), (key, val) => { sliced[key] = val; }); if(errs.length === 0) return sliced as UnwrappedTypeStruct; return new Err(`${val} failed the following checks:\n${errs.join('\n')}`); } private checkType(val: any): Err> | undefined { if(typeof val !== 'object') return new Err(`${val} is not an object`); if(Array.isArray(val)) return new Err(`${val} is an array`); if(val === null) return new Err(`${val} is null`); return undefined; } private checkFields(val: any, checkFn: (t: Type, val: any) => Result, collect?: (key: string, val: any) => any): string[] { const errs: string[] = []; for(const prop in this.definition) { const field = this.definition[prop] if(!(prop in val)) { if(allowsMissing(field)) continue; if(allowsOptional(field)) continue; errs.push(`missing key '${prop}'`); continue; } const valField = val[prop]; if(valField === undefined && allowsOptional(field)) { if(collect) collect(prop, undefined); continue; } const result = checkFn(keyType(field), valField); if(result instanceof Err) errs.push(result.message); if(collect) collect(prop, result); } if(this.exact && typeof val === 'object') { for(const prop in val) { if(!(prop in this.definition)) { errs.push(`unknown key ${prop}`); } } } return errs; } } export function subtype(def: T): Struct { return new Struct(def, false); } export function exact(def: T): Struct { return new Struct(def, true); } export function optional>(check: T): OptionalKey { return new OptionalKey(check); } export function allowMissing>(check: T): MissingKey { return new MissingKey(check); } type MakeOptional = T extends Type ? OptionalKey : T extends MissingKey ? OptionalKey : T; type DeepPartialTypeStruct = { [K in keyof T]: T[K] extends Struct ? OptionalKey>> : MakeOptional } type PartialTypeStruct = { [K in keyof T]: MakeOptional }; export class PartialStruct extends MergeableType>> { private readonly hiddenStruct: Struct>; private readonly hiddenTypeStruct: PartialTypeStruct; constructor(readonly struct: Struct) { super(); const partialDef: Partial> = {}; for(const k in struct.definition) { const v = struct.definition[k]; if(v instanceof MissingKey) { //@ts-ignore partialDef[k] = optional(v.type); } else if(v instanceof OptionalKey) { //@ts-ignore partialDef[k] = optional(v.type); } else { //@ts-ignore partialDef[k] = optional(v); } } this.hiddenTypeStruct = partialDef as PartialTypeStruct; this.hiddenStruct = new Struct(this.hiddenTypeStruct, struct.exact); } check(val: any): Result>> { return this.hiddenStruct.check(val); } sliceResult(val: any): Result>> { return this.hiddenStruct.sliceResult(val); } reify(): Struct> { return new Struct(this.hiddenTypeStruct, this.hiddenStruct.exact); } } export function deepPartial(ogstruct: Struct): PartialStruct> { // If the original struct isn't nested, it's just an ordinary partial call. Don't modify the // definition, or else when you convert to TypeScript it won't correctly ref out the struct if(!hasNested(ogstruct)) { // @ts-ignore return partial(ogstruct); } // If we got this far, the struct has nesting, and therefore can't simply be ref-ed out when // converting to TypeScript. We must modify it recursively. const partialDef: Partial> = {}; for(const k in ogstruct.definition) { const v = ogstruct.definition[k]; if(v instanceof MissingKey) { //@ts-ignore partialDef[k] = new MissingKey(v.type); } else if(v instanceof OptionalKey) { //@ts-ignore partialDef[k] = optional(v.type); } else { const deepKind = deepPartialKind(v); // @ts-ignore partialDef[k] = deepKind; } } const struct = new Struct(partialDef as DeepPartialTypeStruct, ogstruct.exact); return new PartialStruct(struct); } export class MergeIntersect< LVal, RVal, L extends MergeableType, R extends MergeableType, > extends MergeableType { // DANGER: NEVER EVER LEAK THIS OBJECT // This is for internal use only. Do not use its .and function. Do not pass it to other .and or // .or functions. Do not use it AT ALL except to call .check or .sliceResult on it. It MAY be a // secret internal class called InternalDictStructMerge (defined below) that does not play well // with other types or type algebra. protected readonly merged: Type; constructor(readonly l: L, readonly r: R) { super(); this.merged = merge(l, { dict: l => this.mergeDictAndMergeable(l, r), struct: l => this.mergeStructAndMergeable(l, r), partial: l => this.mergeStructAndMergeable(l.reify(), r), merge: l => this.mergeIntersectAndMergeable(l, r), internal: () => { throw `leaked internal dict/struct merge class; structural error`; }, }); } check(val: any) { return this.merged.check(val); } sliceResult(val: any) { return this.merged.sliceResult(val); } private mergeDictAndMergeable( d: Dict, m: MergeableType | InternalDictStructMerge ): MergeableType | InternalDictStructMerge { return merge(m, { dict: (m) => this.mergeDicts(d, m), partial: (m) => new InternalDictStructMerge(m.reify(), d), struct: (m) => this.mergeDictAndStruct(d, m), merge: (m) => this.mergeDictAndMergeable(d, m.merged), internal: (m) => this.mergeInternalAndDict(m, d), }); } private mergeStructAndMergeable( s: Struct, m: MergeableType | InternalDictStructMerge ): MergeableType | InternalDictStructMerge { return merge(m, { dict: m => this.mergeDictAndStruct(m, s), partial: m => this.mergeStructs(s, m.reify()), struct: m => this.mergeStructs(s, m), merge: m => this.mergeStructAndMergeable(s, m.merged), internal: m => this.mergeInternalAndStruct(m, s), }); } private mergePartialAndMergeable( p: PartialStruct, m: MergeableType | InternalDictStructMerge, ): MergeableType | InternalDictStructMerge { return merge(m, { dict: m => new InternalDictStructMerge(p.reify(), m), partial: m => this.mergeStructs(p.reify(), m.reify()), struct: m => this.mergeStructs(p.reify(), m), merge: m => this.mergePartialAndMergeable(p, m.merged), internal: m => this.mergeInternalAndStruct(m, p.reify()), }); } private mergeIntersectAndMergeable( i: MergeIntersect, m: MergeableType | InternalDictStructMerge ): MergeableType | InternalDictStructMerge { return merge(m, { dict: m => this.mergeDictAndMergeable(m, i.merged), partial: m => this.mergePartialAndMergeable(m, i.merged), struct: m => this.mergeStructAndMergeable(m, i.merged), merge: m => this.mergeIntersectAndMergeable(m, i.merged), internal: m => this.mergeInternalAndIntersect(m, i), }); } private mergeInternalAndDict(l: InternalDictStructMerge, r: Dict) { return new InternalDictStructMerge(l.s, this.mergeDicts(l.d, r)); } private mergeInternalAndStruct(l: InternalDictStructMerge, r: Struct) { return new InternalDictStructMerge(this.mergeStructs(l.s, r), l.d); } private mergeInternalAndIntersect( l: InternalDictStructMerge, r: MergeIntersect ): MergeableType | InternalDictStructMerge { return merge(r.merged, { dict: merged => this.mergeInternalAndDict(l, merged), partial: merged => this.mergeInternalAndStruct(l, merged.reify()), struct: merged => this.mergeInternalAndStruct(l, merged), merge: () => { throw `MergeIntersect can't be a child of a MergeIntersect; structural internal error` }, internal: merged => this.mergeInternalAndInternal(l, merged), }); } private mergeInternalAndInternal( l: InternalDictStructMerge, r: InternalDictStructMerge, ) { return new InternalDictStructMerge( this.mergeStructs(l.s, r.s), this.mergeDicts(l.d, r.d), ) } private mergeDicts(l: Dict, r: Dict): Dict { return dict(l.valueType.and(r.valueType)); } private mergeDictAndStruct(l: Dict, r: StructFor) { return new InternalDictStructMerge(r, l); } private mergeStructs(l: Struct, r: Struct) { const definition: { [key: string]: FieldDef } = {}; for(const prop in l.definition) { definition[prop] = l.definition[prop]; } for(const prop in r.definition) { const existing = definition[prop]; const merging = r.definition[prop]; // If it's an additional key, slap it in if(existing == null) definition[prop] = r.definition[prop]; // If it's a missing key, handle else if(existing instanceof MissingKey) { // Missing keys are stricter than optional, so both converge to missing key if(merging instanceof MissingKey || merging instanceof OptionalKey) { definition[prop] = new MissingKey(existing.type.and(merging.type)); } // The strictest is just a raw type, so unwrap and merge else { definition[prop] = existing.type.and(merging); } } // If it's an optional key, handle else if(existing instanceof OptionalKey) { // Missing key is stricter than optional, so it wins if(merging instanceof MissingKey) { definition[prop] = new MissingKey(existing.type.and(merging.type)); } // Two optionals merge into an optional else if(merging instanceof OptionalKey) { definition[prop] = new OptionalKey(existing.type.and(merging.type)); } // A raw type is stricter than optional, so it unwraps the optionality else { definition[prop] = existing.type.and(merging); } } else { // Raw types are stricter than missing or optional types if(merging instanceof MissingKey || merging instanceof OptionalKey) { definition[prop] = existing.and(merging.type); } // Finally, merging two raw types else { definition[prop] = existing.and(merging); } } } return new Struct(definition, l.exact && r.exact); } } type MergeHandlers = { dict: (d: Dict) => MergeResult, partial: (p: PartialStruct) => MergeResult, struct: (s: Struct) => MergeResult, merge: (m: MergeIntersect) => MergeResult, internal: (i: InternalDictStructMerge) => MergeResult, }; type MergeResult = MergeableType | InternalDictStructMerge; function merge>( i: Input, handlers: MergeHandlers, ): MergeResult { if(i instanceof Dict) return handlers.dict(i); if(i instanceof PartialStruct) return handlers.partial(i); if(i instanceof Struct) return handlers.struct(i); if(i instanceof MergeIntersect) return handlers.merge(i); if(i instanceof InternalDictStructMerge) return handlers.internal(i); throw `Unknown type ${i}`; } class InternalDictStructMerge< SVal extends TypeStruct, DVal, S extends Struct, D extends Dict > extends Type & GetType> { readonly s: S; constructor(s: S, readonly d: D) { super(); this.s = new Struct(s.definition, false) as S; } check(val: any) { const dResult = this.d.check(val); if(dResult instanceof Err) return dResult; const sResult = this.s.check(val); if(sResult instanceof Err) return sResult; return val; } sliceResult(val: any) { const dResult = this.d.sliceResult(val); if(dResult instanceof Err) return dResult; const sResult = this.s.sliceResult(val); return Object.assign({}, dResult, sResult) as GetType & GetType; } } export const Nested = [ Struct, PartialStruct, Dict, Either, DefaultIntersect, MergeIntersect, Comment, ] as const; export type NestedType = InstanceType<(typeof Nested)[number]>; function deepPartialKind(kind: Type): Type { if(isNested(kind)) return handleNested(kind); return kind; } function handleNested(kind: NestedType): Type { if(kind instanceof Struct) { if(hasNested(kind)) return deepPartial(kind); return new PartialStruct(kind); } if(kind instanceof PartialStruct) return deepPartial(kind.struct); if(kind instanceof Comment) return new Comment(kind.commentStr, deepPartialKind(kind.wrapped)); if(kind instanceof Dict) return new Dict(deepPartialKind(kind.valueType), kind.namedKey); if(kind instanceof Either) return new Either(deepPartialKind(kind.l), deepPartialKind(kind.r)); if(kind instanceof MergeIntersect) return new DefaultIntersect(deepPartialKind(kind.l), deepPartialKind(kind.r)); return new DefaultIntersect(deepPartialKind(kind.l), deepPartialKind(kind.r)); } function isNested(kind: Type | NestedType): kind is NestedType { for(const t of Nested) { if(kind instanceof t) return true; } return false; } function hasNested(struct: Struct) { for(const k in struct.definition) { if(isNested(struct.definition[k])) return true; } return false; } export function partial(struct: Struct): PartialStruct { return new PartialStruct(struct); }