Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add InferUncoerced type utility #1181

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 23 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,10 @@ 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 +183,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 +225,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
18 changes: 9 additions & 9 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>,
condition: Struct<C, any>,
export function coerce<T, S, C, UC>(
struct: Struct<T, S, UC>,
condition: Struct<C, any, any>,
coercer: Coercer<C>
): Struct<T, S> {
): Struct<T, S, UC | C | T> {
return new Struct({
...struct,
coercer: (value, ctx) => {
Expand All @@ -35,14 +35,14 @@ 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> {
return coerce(struct, unknown(), (x) => {
): Struct<T, S, undefined | Partial<T> | C> {
return coerce(struct, unknown() as Struct<undefined | Partial<T>>, (x) => {
const f = typeof fallback === 'function' ? fallback() : fallback

if (x === undefined) {
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>(struct: Struct<T, S>): Struct<T, S, string | T> {
return coerce(struct, string(), (x) => x.trim())
}
34 changes: 26 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,9 @@ 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 +174,11 @@ 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 +270,9 @@ 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 +303,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 +442,11 @@ 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 +479,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 +508,11 @@ 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
12 changes: 9 additions & 3 deletions src/structs/utilities.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -164,9 +170,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
53 changes: 46 additions & 7 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -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'

/**
Expand Down Expand Up @@ -56,10 +63,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 +102,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 +126,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 +298,14 @@ 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 +429,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]>]>