diff --git a/src/struct.ts b/src/struct.ts index b482fd3b..3a8c4b83 100644 --- a/src/struct.ts +++ b/src/struct.ts @@ -7,8 +7,9 @@ import { StructError, Failure } from './error' * validate unknown input data against the struct. */ -export class Struct { +export class Struct { readonly TYPE!: T + readonly UNCOERCED_TYPE!: C type: string schema: S coercer: (value: unknown, context: Context) => unknown @@ -25,7 +26,7 @@ export class Struct { coercer?: Coercer validator?: Validator refiner?: Refiner - entries?: Struct['entries'] + entries?: Struct['entries'] }) { const { type, @@ -117,9 +118,9 @@ export class Struct { * Assert that a value passes a struct, throwing if it doesn't. */ -export function assert( +export function assert( value: unknown, - struct: Struct, + struct: Struct, message?: string ): asserts value is T { const result = validate(value, struct, { message }) @@ -133,9 +134,9 @@ export function assert( * Create a value with the coercion logic of struct and validate it. */ -export function create( +export function create( value: unknown, - struct: Struct, + struct: Struct, message?: string ): T { const result = validate(value, struct, { coerce: true, message }) @@ -151,9 +152,9 @@ export function create( * Mask a value, returning only the subset of properties defined by a struct. */ -export function mask( +export function mask( value: unknown, - struct: Struct, + struct: Struct, message?: string ): T { const result = validate(value, struct, { coerce: true, mask: true, message }) @@ -169,7 +170,7 @@ export function mask( * Check if a value passes a struct. */ -export function is(value: unknown, struct: Struct): value is T { +export function is(value: unknown, struct: Struct): value is T { const result = validate(value, struct) return !result[0] } @@ -179,9 +180,9 @@ export function is(value: unknown, struct: Struct): value is T { * value (with potential coercion) if valid. */ -export function validate( +export function validate( value: unknown, - struct: Struct, + struct: Struct, options: { coerce?: boolean mask?: boolean @@ -221,7 +222,14 @@ export type Context = { * A type utility to extract the type from a `Struct` class. */ -export type Infer> = T['TYPE'] +export type Infer> = T['TYPE'] + +/** + * A type utility to extract the type from a `Struct` class before coercion + */ + +export type InferUncoerced> = T['UNCOERCED_TYPE'] + /** * A type utility to describe that a struct represents a TypeScript type. diff --git a/src/structs/coercions.ts b/src/structs/coercions.ts index 6dca4ece..a8bea1c2 100644 --- a/src/structs/coercions.ts +++ b/src/structs/coercions.ts @@ -13,11 +13,11 @@ import { string, unknown } from './types' * take effect! Using simply `assert()` or `is()` will not use coercion. */ -export function coerce( - struct: Struct, +export function coerce( + struct: Struct, condition: Struct, coercer: Coercer -): Struct { +): Struct { return new Struct({ ...struct, coercer: (value, ctx) => { @@ -35,13 +35,13 @@ export function coerce( * take effect! Using simply `assert()` or `is()` will not use coercion. */ -export function defaulted( - struct: Struct, +export function defaulted( + struct: Struct, fallback: any, options: { strict?: boolean } = {} -): Struct { +): Struct { return coerce(struct, unknown(), (x) => { const f = typeof fallback === 'function' ? fallback() : fallback @@ -76,6 +76,6 @@ export function defaulted( * take effect! Using simply `assert()` or `is()` will not use coercion. */ -export function trimmed(struct: Struct): Struct { +export function trimmed(struct: Struct): Struct { return coerce(struct, string(), (x) => x.trim()) } diff --git a/src/structs/types.ts b/src/structs/types.ts index 8bcc0ef3..b337a8aa 100644 --- a/src/structs/types.ts +++ b/src/structs/types.ts @@ -1,4 +1,4 @@ -import { Infer, Struct } from '../struct' +import { Infer, InferUncoerced, Struct } from '../struct' import { define } from './utilities' import { ObjectSchema, @@ -9,6 +9,8 @@ import { AnyStruct, InferStructTuple, UnionToIntersection, + ObjectTypeUncoerced, + InferStructTupleUncoerced, } from '../utils' /** @@ -27,7 +29,7 @@ export function any(): Struct { * and it is preferred to using `array(any())`. */ -export function array>(Element: T): Struct[], T> +export function array>(Element: T): Struct[], T, InferUncoerced[]> export function array(): Struct export function array>(Element?: T): any { return new Struct({ @@ -170,7 +172,7 @@ export function integer(): Struct { export function intersection( Structs: [A, ...B] -): Struct & UnionToIntersection[number]>, null> { +): Struct & UnionToIntersection[number]>, null, InferUncoerced & UnionToIntersection[number]>> { return new Struct({ type: 'intersection', schema: null, @@ -262,7 +264,7 @@ export function never(): Struct { * Augment an existing struct to allow `null` values. */ -export function nullable(struct: Struct): Struct { +export function nullable(struct: Struct): Struct { return new Struct({ ...struct, validator: (value, ctx) => value === null || struct.validator(value, ctx), @@ -293,7 +295,7 @@ export function number(): Struct { export function object(): Struct, null> export function object( schema: S -): Struct, S> +): Struct, S, ObjectTypeUncoerced> export function object(schema?: S): any { const knowns = schema ? Object.keys(schema) : [] const Never = never() @@ -432,7 +434,7 @@ export function string(): Struct { export function tuple( Structs: [A, ...B] -): Struct<[Infer, ...InferStructTuple], null> { +): Struct<[Infer, ...InferStructTuple], null, [InferUncoerced, ...InferStructTupleUncoerced]> { const Never = never() return new Struct({ @@ -465,7 +467,7 @@ export function tuple( export function type( schema: S -): Struct, S> { +): Struct, S, ObjectTypeUncoerced> { const keys = Object.keys(schema) return new Struct({ type: 'type', @@ -494,7 +496,7 @@ export function type( export function union( Structs: [A, ...B] -): Struct | InferStructTuple[number], null> { +): Struct | InferStructTuple[number], null, InferUncoerced | InferStructTupleUncoerced[number]> { const description = Structs.map((s) => s.type).join(' | ') return new Struct({ type: 'union', diff --git a/src/structs/utilities.ts b/src/structs/utilities.ts index 28c1c53b..b711ff87 100644 --- a/src/structs/utilities.ts +++ b/src/structs/utilities.ts @@ -1,6 +1,6 @@ import { Struct, Context, Validator } from '../struct' import { object, optional, type } from './types' -import { ObjectSchema, Assign, ObjectType, PartialObjectSchema } from '../utils' +import { ObjectSchema, Assign, ObjectType, PartialObjectSchema, ObjectTypeUncoerced } from '../utils' /** * Create a new struct that combines the properties properties from multiple @@ -164,9 +164,9 @@ export function lazy(fn: () => Struct): Struct { */ export function omit( - struct: Struct, S>, + struct: Struct, S, ObjectTypeUncoerced>, keys: K[] -): Struct>, Omit> { +): Struct>, Omit, ObjectTypeUncoerced>> { const { schema } = struct const subschema: any = { ...schema } diff --git a/src/utils.ts b/src/utils.ts index 92df9755..0fcdd271 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -import { Struct, Infer, Result, Context, Describe } from './struct' +import { Struct, Infer, Result, Context, Describe, InferUncoerced } from './struct' import { Failure } from './error' /** @@ -56,10 +56,10 @@ export function shiftIterator(input: Iterator): T | undefined { * Convert a single validation result to a failure. */ -export function toFailure( +export function toFailure( result: string | boolean | Partial, context: Context, - struct: Struct, + struct: Struct, value: any ): Failure | undefined { if (result === true) { @@ -95,10 +95,10 @@ export function toFailure( * Convert a validation result to an iterable of failures. */ -export function* toFailures( +export function* toFailures( result: Result, context: Context, - struct: Struct, + struct: Struct, value: any ): IterableIterator { if (!isIterable(result)) { @@ -119,9 +119,9 @@ export function* toFailures( * returning an iterator of failures or success. */ -export function* run( +export function* run( value: unknown, - struct: Struct, + struct: Struct, options: { path?: any[] branch?: any[] @@ -291,6 +291,15 @@ export type ObjectType = Simplify< Optionalize<{ [K in keyof S]: Infer }> > +/** + * Infer a type from an object struct schema. + */ + +export type ObjectTypeUncoerced = Simplify< + Optionalize<{ [K in keyof S]: InferUncoerced }> +> + + /** * Omit properties from a type that extend from a specific type. */ @@ -414,3 +423,27 @@ type _InferTuple< > = Index extends Length ? Accumulated : _InferTuple]> + + /** + * Infer a tuple of types from a tuple of `Struct`s. + * + * This is used to recursively retrieve the type from `union` `intersection` and + * `tuple` structs. + */ + +export type InferStructTupleUncoerced< +Tuple extends AnyStruct[], +Length extends number = Tuple['length'] +> = Length extends Length +? number extends Length + ? Tuple + : _InferTupleUncoerced +: never +type _InferTupleUncoerced< +Tuple extends AnyStruct[], +Length extends number, +Accumulated extends unknown[], +Index extends number = Accumulated['length'] +> = Index extends Length +? Accumulated +: _InferTuple]>