diff --git a/ark/type/methods/base.ts b/ark/type/methods/base.ts index df69a9258..91cf04e43 100644 --- a/ark/type/methods/base.ts +++ b/ark/type/methods/base.ts @@ -42,12 +42,27 @@ import type { instantiateType } from "./instantiate.ts" interface Type extends Callable<(data: unknown) => distill.Out | ArkErrors> { [inferred]: t + /** + * The top-level generic parameter accepted by the `Type`.\ + * Potentially includes morphs and subtype constraints not reflected + * in the types fully-inferred input (via `inferIn`) or output (via `infer` or `inferOut`). + * @example type A = type.infer<[typeof T.t, '[]']> + */ t: t + /** + * A type representing the output the `Type` will return (after morphs are applied to valid input) + * @example export type MyType = typeof MyType.infer + * @example export interface MyType extends Identity {} + */ infer: this["inferOut"] inferBrandableIn: distill.brandable.In inferBrandableOut: distill.brandable.Out inferIntrospectableOut: distill.introspectable.Out inferOut: distill.Out + /** + * A type representing the input the `Type` will accept (before morphs are applied) + * @example export type MyTypeInput = typeof MyType.inferIn + */ inferIn: distill.In inferredOutIsIntrospectable: t extends InferredMorph ? [o] extends [anyOrNever] ? true @@ -57,6 +72,7 @@ interface Type unknown extends t ? boolean : true + /** Internal JSON representation of this `Type` */ json: Json toJSON(): Json meta: ArkAmbient.meta @@ -67,8 +83,18 @@ interface Type internal: BaseRoot $: Scope<$> + /** + * Validate data, throwing `type.errors` instance on failure + * @returns a valid value + * @example const validData = T.assert(rawData) + */ assert(data: unknown): this["infer"] + /** + * Check if data matches the input shape. + * Doesn't process any morphs, but does check narrows. + * @example type({ foo: "number" }).allows({ foo: "bar" }) // false + */ allows(data: unknown): data is this["inferIn"] traverse(data: unknown): this["infer"] | ArkErrors @@ -79,36 +105,88 @@ interface Type describe(description: string): this + /** + * Create a copy of this `Type` with updated unknown key behavior + * - `ignore`: ignore unknown properties (default) + * - 'reject': disallow objects with unknown properties + * - 'delete': clone the object and keep only known properties + */ onUndeclaredKey(behavior: UndeclaredKeyBehavior): this + /** + * Create a copy of this `Type` with updated unknown key behavior\ + * The behavior applies to the whole object tree, not just the immediate properties. + * - `ignore`: ignore unknown properties (default) + * - 'reject': disallow objects with unknown properties + * - 'delete': clone the object and keep only known properties + */ onDeepUndeclaredKey(behavior: UndeclaredKeyBehavior): this + /** + * Identical to `assert`, but with a typed input as a convenience for providing a typed value. + * @example const ConfigT = type({ foo: "string" }); export const config = ConfigT.from({ foo: "bar" }) + */ from(literal: this["inferIn"]): this["infer"] + /** + * Cast the way this `Type` is inferred (has no effect at runtime). + * const branded = type(/^a/).as<`a${string}`>() // Type<`a${string}`> + */ as(...args: validateChainedAsArgs): instantiateType // brand>>( // name: name // ): instantiateType + /** + * A `Type` representing the deeply-extracted input of the `Type` (before morphs are applied). + * @example const inputT = T.in + */ get in(): instantiateType + /** + * A `Type` representing the deeply-extracted output of the `Type` (after morphs are applied).\ + * **IMPORTANT**: If your type includes morphs, their output will likely be unknown + * unless they were defined with an explicit output validator via `.to(outputType)`, `.pipe(morph, outputType)`, etc. + * @example const outputT = T.out + */ get out(): instantiateType // inferring r into an alias improves perf and avoids return type inference // that can lead to incorrect results. See: // https://discord.com/channels/957797212103016458/1285420361415917680/1285545752172429312 + /** + * Intersect another `Type` definition, returning an introspectable `Disjoint` if the result is unsatisfiable. + * @example const intersection = type({ foo: "number" }).intersect({ bar: "string" }) // Type<{ foo: number; bar: string }> + * @example const intersection = type({ foo: "number" }).intersect({ foo: "string" }) // Disjoint + */ intersect>( def: type.validate ): instantiateType, $> | Disjoint + /** + * Intersect another `Type` definition, throwing an error if the result is unsatisfiable. + * @example const intersection = type({ foo: "number" }).intersect({ bar: "string" }) // Type<{ foo: number; bar: string }> + */ and>( def: type.validate ): instantiateType, $> + /** + * Union another `Type` definition.\ + * If the types contain morphs, input shapes should be distinct. Otherwise an error will be thrown. + * @example const union = type({ foo: "number" }).or({ foo: "string" }) // Type<{ foo: number } | { foo: string }> + * @example const union = type("string.numeric.parse").or("number") // Type<((In: string) => Out) | number> + */ or>( def: type.validate ): instantiateType + /** + * Add a custom predicate to this `Type`. + * @example const nan = type('number').narrow(n => Number.isNaN(n)) // Type + * @example const foo = type("string").narrow((s): s is `foo${string}` => s.startsWith('foo') || ctx.mustBe('string starting with "foo"')) // Type<"foo${string}"> + * @example const unique = type('string[]').narrow((a, ctx) => new Set(a).size === a.length || ctx.mustBe('array with unique elements')) + */ narrow< narrowed extends this["infer"] = never, r = [narrowed] extends [never] ? @@ -140,8 +218,17 @@ interface Type | PredicateCast ): instantiateType + /** + * Create a `Type` for array with elements of this `Type` + * @example const T = type(/^foo/); const array = T.array() // Type + */ array(): ArrayType + /** + * Morph this `Type` through a chain of morphs. + * @example const dedupe = type('string[]').pipe(a => Array.from(new Set(a))) + * @example type({codes: 'string.numeric[]'}).pipe(obj => obj.codes).to('string.numeric.parse[]') + */ pipe: ChainedPipes equals(def: type.validate): boolean @@ -175,6 +262,14 @@ interface Type // work the way it does for the other methods here optional>(): instantiateType + /** + * Add a default value for this `Type` when it is used as a property.\ + * Default value should be a valid input value for this `Type, or a function that returns a valid input value.\ + * If the type has a morph, it will be applied to the default value. + * @example const withDefault = type({ foo: type("string").default("bar") }); withDefault({}) // { foo: "bar" } + * @example const withFactory = type({ foo: type("number[]").default(() => [1])) }); withFactory({baz: 'a'}) // { foo: [1], baz: 'a' } + * @example const withMorph = type({ foo: type("string.numeric.parse").default("123") }); withMorph({}) // { foo: 123 } + */ default< const value extends this["inferIn"], r = applyAttribute> diff --git a/ark/type/methods/morph.ts b/ark/type/methods/morph.ts index 39260fa19..df6adbb80 100644 --- a/ark/type/methods/morph.ts +++ b/ark/type/methods/morph.ts @@ -6,6 +6,10 @@ import type { BaseType } from "./base.ts" // non-morph branches /** @ts-ignore cast variance */ interface Type extends BaseType { + /** + * Append extra validation shape on the pipe output + * @example type({codes: 'string.numeric[]'}).pipe(obj => obj.codes).to('string.numeric.parse[]') + */ to>( def: type.validate ): Type, $> diff --git a/ark/type/methods/object.ts b/ark/type/methods/object.ts index 399e56a72..22b280949 100644 --- a/ark/type/methods/object.ts +++ b/ark/type/methods/object.ts @@ -37,6 +37,10 @@ interface Type extends BaseType { keyof(): instantiateType, $> + /** + * Get the `Type` of a property of this `Type`. + * @example type({ foo: "string" }).get("foo") // Type + */ get, r = arkGet>( k1: k1 | type.cast ): instantiateType @@ -58,6 +62,10 @@ interface Type extends BaseType { k3: k3 | type.cast ): instantiateType + /** + * Create a copy of this `Type` with only the specified properties. + * @example type({ foo: "string", bar: "number" }).pick("foo") // Type<{ foo: string }> + */ pick = never>( ...keys: (key | type.cast)[] ): Type< @@ -67,6 +75,10 @@ interface Type extends BaseType { $ > + /** + * Create a copy of this `Type` with all properties except the specified ones. + * @example type({ foo: "string", bar: "number" }).omit("foo") // Type<{ bar: number }> + */ omit = never>( ...keys: (key | type.cast)[] ): Type< @@ -76,14 +88,26 @@ interface Type extends BaseType { $ > + /** + * Merge another `Type` definition, overriding properties of this `Type` with the duplicate keys. + * @example type({ a: "1", b: "2" }).merge({ b: "3", c: "4" }) // Type<{ a: 1, b: 3, c: 4 }> + */ merge>( def: type.validate & (r extends object ? unknown : ErrorType<"Merged type must be an object", [actual: r]>) ): Type, $> + /** + * Create a copy of this `Type` with all properties required. + * @example const T = type({ "foo?"": "string" }).required() // Type<{ foo: string }> + */ required(): Type<{ [k in keyof t]-?: t[k] }, $> + /** + * Create a copy of this `Type` with all properties optional. + * @example: const T = type({ foo: "string" }).optional() // Type<{ foo?: string }> + */ partial(): Type<{ [k in keyof t]?: t[k] }, $> map>( @@ -91,6 +115,10 @@ interface Type extends BaseType { flatMapEntry: (entry: typePropOf) => transformed ): Type, $> + /** + * List of property info of this `Type`. + * @example type({ foo: "string = "" }).props // [{ kind: "required", key: "foo", value: Type, default: "" }] + */ props: array> } diff --git a/ark/type/type.ts b/ark/type/type.ts index a4838dae9..86a5aca21 100644 --- a/ark/type/type.ts +++ b/ark/type/type.ts @@ -86,6 +86,11 @@ export interface TypeParser<$ = {}> extends Ark.boundTypeAttachments<$> { : [] ): r + /** + * Error class for validation errors + * Calling type instance returns an instance of this class on failure + * @example if ( T(data) instanceof type.errors ) { ... } + */ errors: typeof ArkErrors hkt: typeof Hkt keywords: typeof keywords @@ -96,7 +101,18 @@ export interface TypeParser<$ = {}> extends Ark.boundTypeAttachments<$> { define: DefinitionParser<$> generic: GenericParser<$> schema: SchemaParser<$> + /** + * Create a `Type` that is satisfied only by a value strictly equal (`===`) to the argument passed to this function. + * @example const foo = type.unit('foo') // Type<'foo'> + * @example const sym: unique symbol = Symbol(); type.unit(sym) // Type + */ unit: UnitTypeParser<$> + /** + * Create a `Type` that is satisfied only by a value strictly equal (`===`) to one of the arguments passed to this function. + * @example const enum = type.enumerated('foo', 'bar', obj) // obj is a by-reference object + * @example const tupleForm = type(['===', 'foo', 'bar', obj]) + * @example const argsForm = type('===', 'foo', 'bar', obj) + */ enumerated: EnumeratedTypeParser<$> }