Skip to content

Commit

Permalink
feat: add InferUncoerced type utility
Browse files Browse the repository at this point in the history
Works exactly like Infer, except where part of the schema is coerced,
returns the un-coerced type instead.
  • Loading branch information
ziad-saab committed May 10, 2023
1 parent 03d65bd commit 702d1e9
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 37 deletions.
32 changes: 20 additions & 12 deletions src/struct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import { StructError, Failure } from './error'
* validate unknown input data against the struct.
*/

export class Struct<T = unknown, S = unknown> {
export class Struct<T = unknown, S = unknown, C = T> {
readonly TYPE!: T
readonly UNCOERCED_TYPE!: C
type: string
schema: S
coercer: (value: unknown, context: Context) => unknown
Expand All @@ -25,7 +26,7 @@ export class Struct<T = unknown, S = unknown> {
coercer?: Coercer
validator?: Validator
refiner?: Refiner<T>
entries?: Struct<T, S>['entries']
entries?: Struct<T, S, C>['entries']
}) {
const {
type,
Expand Down Expand Up @@ -117,9 +118,9 @@ export class Struct<T = unknown, S = unknown> {
* Assert that a value passes a struct, throwing if it doesn't.
*/

export function assert<T, S>(
export function assert<T, S, C>(
value: unknown,
struct: Struct<T, S>,
struct: Struct<T, S, C>,
message?: string
): asserts value is T {
const result = validate(value, struct, { message })
Expand All @@ -133,9 +134,9 @@ export function assert<T, S>(
* Create a value with the coercion logic of struct and validate it.
*/

export function create<T, S>(
export function create<T, S, C>(
value: unknown,
struct: Struct<T, S>,
struct: Struct<T, S, C>,
message?: string
): T {
const result = validate(value, struct, { coerce: true, message })
Expand All @@ -151,9 +152,9 @@ export function create<T, S>(
* Mask a value, returning only the subset of properties defined by a struct.
*/

export function mask<T, S>(
export function mask<T, S, C>(
value: unknown,
struct: Struct<T, S>,
struct: Struct<T, S, C>,
message?: string
): T {
const result = validate(value, struct, { coerce: true, mask: true, message })
Expand All @@ -169,7 +170,7 @@ export function mask<T, S>(
* Check if a value passes a struct.
*/

export function is<T, S>(value: unknown, struct: Struct<T, S>): value is T {
export function is<T, S, C>(value: unknown, struct: Struct<T, S, C>): value is T {
const result = validate(value, struct)
return !result[0]
}
Expand All @@ -179,9 +180,9 @@ export function is<T, S>(value: unknown, struct: Struct<T, S>): value is T {
* value (with potential coercion) if valid.
*/

export function validate<T, S>(
export function validate<T, S, C>(
value: unknown,
struct: Struct<T, S>,
struct: Struct<T, S, C>,
options: {
coerce?: boolean
mask?: boolean
Expand Down Expand Up @@ -221,7 +222,14 @@ export type Context = {
* A type utility to extract the type from a `Struct` class.
*/

export type Infer<T extends Struct<any, any>> = T['TYPE']
export type Infer<T extends Struct<any, any, any>> = T['TYPE']

/**
* A type utility to extract the type from a `Struct` class before coercion
*/

export type InferUncoerced<T extends Struct<any, any, any>> = T['UNCOERCED_TYPE']


/**
* A type utility to describe that a struct represents a TypeScript type.
Expand Down
14 changes: 7 additions & 7 deletions src/structs/coercions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ import { string, unknown } from './types'
* take effect! Using simply `assert()` or `is()` will not use coercion.
*/

export function coerce<T, S, C>(
struct: Struct<T, S>,
export function coerce<T, S, C, CT>(
struct: Struct<T, S, CT>,
condition: Struct<C, any>,
coercer: Coercer<C>
): Struct<T, S> {
): Struct<T, S, CT> {
return new Struct({
...struct,
coercer: (value, ctx) => {
Expand All @@ -35,13 +35,13 @@ export function coerce<T, S, C>(
* take effect! Using simply `assert()` or `is()` will not use coercion.
*/

export function defaulted<T, S>(
struct: Struct<T, S>,
export function defaulted<T, S, C>(
struct: Struct<T, S, C>,
fallback: any,
options: {
strict?: boolean
} = {}
): Struct<T, S> {
): Struct<T, S, unknown> {
return coerce(struct, unknown(), (x) => {
const f = typeof fallback === 'function' ? fallback() : fallback

Expand Down Expand Up @@ -76,6 +76,6 @@ export function defaulted<T, S>(
* take effect! Using simply `assert()` or `is()` will not use coercion.
*/

export function trimmed<T, S>(struct: Struct<T, S>): Struct<T, S> {
export function trimmed<T, S, C>(struct: Struct<T, S, C>): Struct<T, S, C> {
return coerce(struct, string(), (x) => x.trim())
}
18 changes: 10 additions & 8 deletions src/structs/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Infer, Struct } from '../struct'
import { Infer, InferUncoerced, Struct } from '../struct'
import { define } from './utilities'
import {
ObjectSchema,
Expand All @@ -9,6 +9,8 @@ import {
AnyStruct,
InferStructTuple,
UnionToIntersection,
ObjectTypeUncoerced,
InferStructTupleUncoerced,
} from '../utils'

/**
Expand All @@ -27,7 +29,7 @@ export function any(): Struct<any, null> {
* and it is preferred to using `array(any())`.
*/

export function array<T extends Struct<any>>(Element: T): Struct<Infer<T>[], T>
export function array<T extends Struct<any>>(Element: T): Struct<Infer<T>[], T, InferUncoerced<T>[]>
export function array(): Struct<unknown[], undefined>
export function array<T extends Struct<any>>(Element?: T): any {
return new Struct({
Expand Down Expand Up @@ -170,7 +172,7 @@ export function integer(): Struct<number, null> {

export function intersection<A extends AnyStruct, B extends AnyStruct[]>(
Structs: [A, ...B]
): Struct<Infer<A> & UnionToIntersection<InferStructTuple<B>[number]>, null> {
): Struct<Infer<A> & UnionToIntersection<InferStructTuple<B>[number]>, null, InferUncoerced<A> & UnionToIntersection<InferStructTupleUncoerced<B>[number]>> {
return new Struct({
type: 'intersection',
schema: null,
Expand Down Expand Up @@ -262,7 +264,7 @@ export function never(): Struct<never, null> {
* Augment an existing struct to allow `null` values.
*/

export function nullable<T, S>(struct: Struct<T, S>): Struct<T | null, S> {
export function nullable<T, S, C>(struct: Struct<T, S, C>): Struct<T | null, S> {
return new Struct({
...struct,
validator: (value, ctx) => value === null || struct.validator(value, ctx),
Expand Down Expand Up @@ -293,7 +295,7 @@ export function number(): Struct<number, null> {
export function object(): Struct<Record<string, unknown>, null>
export function object<S extends ObjectSchema>(
schema: S
): Struct<ObjectType<S>, S>
): Struct<ObjectType<S>, S, ObjectTypeUncoerced<S>>
export function object<S extends ObjectSchema>(schema?: S): any {
const knowns = schema ? Object.keys(schema) : []
const Never = never()
Expand Down Expand Up @@ -432,7 +434,7 @@ export function string(): Struct<string, null> {

export function tuple<A extends AnyStruct, B extends AnyStruct[]>(
Structs: [A, ...B]
): Struct<[Infer<A>, ...InferStructTuple<B>], null> {
): Struct<[Infer<A>, ...InferStructTuple<B>], null, [InferUncoerced<A>, ...InferStructTupleUncoerced<B>]> {
const Never = never()

return new Struct({
Expand Down Expand Up @@ -465,7 +467,7 @@ export function tuple<A extends AnyStruct, B extends AnyStruct[]>(

export function type<S extends ObjectSchema>(
schema: S
): Struct<ObjectType<S>, S> {
): Struct<ObjectType<S>, S, ObjectTypeUncoerced<S>> {
const keys = Object.keys(schema)
return new Struct({
type: 'type',
Expand Down Expand Up @@ -494,7 +496,7 @@ export function type<S extends ObjectSchema>(

export function union<A extends AnyStruct, B extends AnyStruct[]>(
Structs: [A, ...B]
): Struct<Infer<A> | InferStructTuple<B>[number], null> {
): Struct<Infer<A> | InferStructTuple<B>[number], null, InferUncoerced<A> | InferStructTupleUncoerced<B>[number]> {
const description = Structs.map((s) => s.type).join(' | ')
return new Struct({
type: 'union',
Expand Down
6 changes: 3 additions & 3 deletions src/structs/utilities.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -164,9 +164,9 @@ export function lazy<T>(fn: () => Struct<T, any>): Struct<T, null> {
*/

export function omit<S extends ObjectSchema, K extends keyof S>(
struct: Struct<ObjectType<S>, S>,
struct: Struct<ObjectType<S>, S, ObjectTypeUncoerced<S>>,
keys: K[]
): Struct<ObjectType<Omit<S, K>>, Omit<S, K>> {
): Struct<ObjectType<Omit<S, K>>, Omit<S, K>, ObjectTypeUncoerced<Omit<S, K>>> {
const { schema } = struct
const subschema: any = { ...schema }

Expand Down
47 changes: 40 additions & 7 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -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'

/**
Expand Down Expand Up @@ -56,10 +56,10 @@ export function shiftIterator<T>(input: Iterator<T>): T | undefined {
* Convert a single validation result to a failure.
*/

export function toFailure<T, S>(
export function toFailure<T, S, C>(
result: string | boolean | Partial<Failure>,
context: Context,
struct: Struct<T, S>,
struct: Struct<T, S, C>,
value: any
): Failure | undefined {
if (result === true) {
Expand Down Expand Up @@ -95,10 +95,10 @@ export function toFailure<T, S>(
* Convert a validation result to an iterable of failures.
*/

export function* toFailures<T, S>(
export function* toFailures<T, S, C>(
result: Result,
context: Context,
struct: Struct<T, S>,
struct: Struct<T, S, C>,
value: any
): IterableIterator<Failure> {
if (!isIterable(result)) {
Expand All @@ -119,9 +119,9 @@ export function* toFailures<T, S>(
* returning an iterator of failures or success.
*/

export function* run<T, S>(
export function* run<T, S, C>(
value: unknown,
struct: Struct<T, S>,
struct: Struct<T, S, C>,
options: {
path?: any[]
branch?: any[]
Expand Down Expand Up @@ -291,6 +291,15 @@ export type ObjectType<S extends ObjectSchema> = Simplify<
Optionalize<{ [K in keyof S]: Infer<S[K]> }>
>

/**
* Infer a type from an object struct schema.
*/

export type ObjectTypeUncoerced<S extends ObjectSchema> = Simplify<
Optionalize<{ [K in keyof S]: InferUncoerced<S[K]> }>
>


/**
* Omit properties from a type that extend from a specific type.
*/
Expand Down Expand Up @@ -414,3 +423,27 @@ type _InferTuple<
> = Index extends Length
? Accumulated
: _InferTuple<Tuple, Length, [...Accumulated, Infer<Tuple[Index]>]>

/**
* 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<Tuple, Length, []>
: never
type _InferTupleUncoerced<
Tuple extends AnyStruct[],
Length extends number,
Accumulated extends unknown[],
Index extends number = Accumulated['length']
> = Index extends Length
? Accumulated
: _InferTuple<Tuple, Length, [...Accumulated, InferUncoerced<Tuple[Index]>]>

0 comments on commit 702d1e9

Please sign in to comment.