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 JSDoc for common Type methods #1157

Merged
merged 4 commits into from
Oct 4, 2024
Merged
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
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"], $>
Dimava marked this conversation as resolved.
Show resolved Hide resolved

// 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