diff --git a/src/struct.ts b/src/struct.ts index b482fd3b..7bc5861c 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,10 @@ 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 +183,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 +225,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..a29a708b 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, - condition: Struct, +export function coerce( + struct: Struct, + condition: Struct, coercer: Coercer -): Struct { +): Struct { return new Struct({ ...struct, coercer: (value, ctx) => { @@ -35,14 +35,14 @@ 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 { - return coerce(struct, unknown(), (x) => { +): Struct | C> { + return coerce(struct, unknown() as Struct>, (x) => { const f = typeof fallback === 'function' ? fallback() : fallback if (x === undefined) { @@ -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..c6758971 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,9 @@ 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 +174,11 @@ export function integer(): Struct { export function intersection( Structs: [A, ...B] -): Struct & UnionToIntersection[number]>, null> { +): Struct< + Infer & UnionToIntersection[number]>, + null, + InferUncoerced & UnionToIntersection[number]> +> { return new Struct({ type: 'intersection', schema: null, @@ -262,7 +270,9 @@ 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 +303,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 +442,11 @@ 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 +479,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 +508,11 @@ export function type( export function union( Structs: [A, ...B] -): Struct | InferStructTuple[number], null> { +): Struct< + Infer | 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..943727e0 100644 --- a/src/structs/utilities.ts +++ b/src/structs/utilities.ts @@ -1,6 +1,12 @@ 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 +170,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..e6db4739 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,11 @@ -import { Struct, Infer, Result, Context, Describe } from './struct' +import { + Struct, + Infer, + Result, + Context, + Describe, + InferUncoerced, +} from './struct' import { Failure } from './error' /** @@ -56,10 +63,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 +102,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 +126,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 +298,14 @@ 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 +429,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]>