Skip to content

Commit

Permalink
feat: add JSDoc for common Type methods (#1157)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dimava authored Oct 4, 2024
1 parent 82ed2e4 commit af48925
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 0 deletions.
95 changes: 95 additions & 0 deletions ark/type/methods/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,27 @@ import type { instantiateType } from "./instantiate.ts"
interface Type<out t = unknown, $ = {}>
extends Callable<(data: unknown) => distill.Out<t> | 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<typeof MyType.infer> {}
*/
infer: this["inferOut"]
inferBrandableIn: distill.brandable.In<t>
inferBrandableOut: distill.brandable.Out<t>
inferIntrospectableOut: distill.introspectable.Out<t>
inferOut: distill.Out<t>
/**
* A type representing the input the `Type` will accept (before morphs are applied)
* @example export type MyTypeInput = typeof MyType.inferIn
*/
inferIn: distill.In<t>
inferredOutIsIntrospectable: t extends InferredMorph<any, infer o> ?
[o] extends [anyOrNever] ? true
Expand All @@ -57,6 +72,7 @@ interface Type<out t = unknown, $ = {}>
unknown extends t ? boolean
: true

/** Internal JSON representation of this `Type` */
json: Json
toJSON(): Json
meta: ArkAmbient.meta
Expand All @@ -67,8 +83,18 @@ interface Type<out t = unknown, $ = {}>
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
Expand All @@ -79,36 +105,88 @@ interface Type<out t = unknown, $ = {}>

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<t = unset>(...args: validateChainedAsArgs<t>): instantiateType<t, $>

// brand<const name extends string, r = applyBrand<t, Predicate<name>>>(
// name: name
// ): instantiateType<r, $>

/**
* A `Type` representing the deeply-extracted input of the `Type` (before morphs are applied).
* @example const inputT = T.in
*/
get in(): instantiateType<this["inferBrandableIn"], $>
/**
* 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<this["inferIntrospectableOut"], $>

// 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<const def, r = type.infer<def, $>>(
def: type.validate<def, $>
): instantiateType<inferIntersection<t, r>, $> | 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<const def, r = type.infer<def, $>>(
def: type.validate<def, $>
): instantiateType<inferIntersection<t, r>, $>

/**
* 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>) | number>
*/
or<const def, r = type.infer<def, $>>(
def: type.validate<def, $>
): instantiateType<t | r, $>

/**
* Add a custom predicate to this `Type`.
* @example const nan = type('number').narrow(n => Number.isNaN(n)) // Type<number>
* @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] ?
Expand Down Expand Up @@ -140,8 +218,17 @@ interface Type<out t = unknown, $ = {}>
| PredicateCast<this["inferIn"], narrowed>
): instantiateType<r, $>

/**
* Create a `Type` for array with elements of this `Type`
* @example const T = type(/^foo/); const array = T.array() // Type<string[]>
*/
array(): ArrayType<t[], $>

/**
* 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<t, $>

equals<const def>(def: type.validate<def, $>): boolean
Expand Down Expand Up @@ -175,6 +262,14 @@ interface Type<out t = unknown, $ = {}>
// work the way it does for the other methods here
optional<r = applyAttribute<t, Optional>>(): instantiateType<r, $>

/**
* 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<t, Default<value>>
Expand Down
4 changes: 4 additions & 0 deletions ark/type/methods/morph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import type { BaseType } from "./base.ts"
// non-morph branches
/** @ts-ignore cast variance */
interface Type<out t = unknown, $ = {}> extends BaseType<t, $> {
/**
* Append extra validation shape on the pipe output
* @example type({codes: 'string.numeric[]'}).pipe(obj => obj.codes).to('string.numeric.parse[]')
*/
to<const def, r = type.infer<def, $>>(
def: type.validate<def, $>
): Type<inferPipe<t, r>, $>
Expand Down
28 changes: 28 additions & 0 deletions ark/type/methods/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ interface Type<out t extends object = object, $ = {}> extends BaseType<t, $> {

keyof(): instantiateType<arkKeyOf<t>, $>

/**
* Get the `Type` of a property of this `Type<object>`.
* @example type({ foo: "string" }).get("foo") // Type<string>
*/
get<const k1 extends arkIndexableOf<t>, r = arkGet<t, k1>>(
k1: k1 | type.cast<k1>
): instantiateType<r, $>
Expand All @@ -58,6 +62,10 @@ interface Type<out t extends object = object, $ = {}> extends BaseType<t, $> {
k3: k3 | type.cast<k3>
): instantiateType<r, $>

/**
* Create a copy of this `Type` with only the specified properties.
* @example type({ foo: "string", bar: "number" }).pick("foo") // Type<{ foo: string }>
*/
pick<const key extends arkKeyOf<t> = never>(
...keys: (key | type.cast<key>)[]
): Type<
Expand All @@ -67,6 +75,10 @@ interface Type<out t extends object = object, $ = {}> extends BaseType<t, $> {
$
>

/**
* 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<const key extends arkKeyOf<t> = never>(
...keys: (key | type.cast<key>)[]
): Type<
Expand All @@ -76,21 +88,37 @@ interface Type<out t extends object = object, $ = {}> extends BaseType<t, $> {
$
>

/**
* 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<const def, r = type.infer<def, $>>(
def: type.validate<def, $> &
(r extends object ? unknown
: ErrorType<"Merged type must be an object", [actual: r]>)
): Type<merge<t, r & object>, $>

/**
* 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<transformed extends listable<MappedTypeProp>>(
// v isn't used directly here but helps TS infer a precise type for transformed
flatMapEntry: (entry: typePropOf<t, $>) => transformed
): Type<constructMapped<t, transformed>, $>

/**
* List of property info of this `Type<object>`.
* @example type({ foo: "string = "" }).props // [{ kind: "required", key: "foo", value: Type<string>, default: "" }]
*/
props: array<typePropOf<t, $>>
}

Expand Down
16 changes: 16 additions & 0 deletions ark/type/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<typeof sym>
*/
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<$>
}

Expand Down

0 comments on commit af48925

Please sign in to comment.