UNPKG

object-shape-tester

Version:
471 lines (470 loc) 17.8 kB
import { type AnyFunction, type ArrayElement, type AtLeastTuple } from '@augment-vir/common'; import { type IsEqual, type IsNever, type LiteralToPrimitive, type Primitive, type Simplify, type UnionToIntersection, type WritableDeep } from 'type-fest'; import { type CustomSpecifier } from './custom-specifier.js'; import { isShapeDefinitionKey, isShapeSpecifierKey } from './shape-keys.js'; /** * ======================================== * * Shape Definition * * ======================================== */ /** * The output of `defineShape`. This is a shape definition which includes the shape itself (used for * shape testing), a `runtimeType`, and a `defaultValue`. * * @category Util */ export type ShapeDefinition<Shape, IsReadonly extends boolean> = { shape: Shape; runtimeType: ShapeToRuntimeType<Shape, false, IsReadonly>; isReadonly: IsReadonly; defaultValue: Readonly<ShapeToRuntimeType<Shape, false, IsReadonly>>; [isShapeDefinitionKey]: true; }; /** * Checks if the given input is a {@link isShapeDefinition}. * * @category Util */ export declare function isShapeDefinition(input: unknown): input is ShapeDefinition<unknown, false>; /** * ======================================== * * Specifier Symbols * * ======================================== */ /** * Values used to mark the outputs of each sub-shape function (like {@link or}). * * @category Internal */ export declare enum ShapeSpecifierType { And = "and", Class = "class", Enum = "enum", Exact = "exact", IndexedKeys = "indexed-keys", Or = "or", Unknown = "unknown", NumericRange = "numeric-range", Optional = "optional", Tuple = "tuple" } /** @category Internal */ export type BaseParts = AtLeastTuple<unknown, 0>; /** * Output from the sub-shape defining functions (such as {@link or}). * * @category Internal */ export type ShapeSpecifier<Parts extends BaseParts, Type extends ShapeSpecifierType> = { [isShapeSpecifierKey]: true; parts: Parts; specifierType: Type; }; /** * Allowed types for keys in the base input for the `indexedKeys` shape. * * @category Internal */ export type AllowedIndexKeysKeysSpecifiers = ShapeEnum<Readonly<[Record<string, number | string>]>> | ShapeExact<Readonly<AtLeastTuple<PropertyKey, 1>>> | ShapeUnknown<[unknown]> | PropertyKey | CustomSpecifier<any>; /** * Base type for inputs to the `indexedKeys` shape. * * @category Internal */ export type BaseIndexedKeys = { keys: ShapeOr<AtLeastTuple<AllowedIndexKeysKeysSpecifiers, 1>> | AllowedIndexKeysKeysSpecifiers; values: unknown; required: boolean; }; /** * ======================================== * * Shape Types * * ======================================== */ /** * {@link ShapeSpecifier} for {@link and}. * * @category Internal */ export type ShapeAnd<Parts extends AtLeastTuple<unknown, 1>> = ShapeSpecifier<Parts, ShapeSpecifierType.And>; /** * Helper type for {@link ShapeClass}. * * @category Internal */ export type AnyConstructor = new (...args: any[]) => any; /** * {@link ShapeSpecifier} for {@link classShape}. * * @category Internal */ export type ShapeClass<Parts extends [AnyConstructor]> = ShapeSpecifier<Parts, ShapeSpecifierType.Class>; /** * {@link ShapeSpecifier} for {@link enumShape}. * * @category Internal */ export type ShapeEnum<Parts extends Readonly<[Record<string, number | string>, (number | string)?]>> = ShapeSpecifier<Parts, ShapeSpecifierType.Enum>; /** * {@link ShapeSpecifier} for {@link exact}. * * @category Internal */ export type ShapeExact<Parts extends Readonly<AtLeastTuple<unknown, 1>>> = ShapeSpecifier<Parts, ShapeSpecifierType.Exact>; /** * {@link ShapeSpecifier} for {@link indexedKeys}. * * @category Internal */ export type ShapeIndexedKeys<Parts extends Readonly<[BaseIndexedKeys]>> = ShapeSpecifier<Parts, ShapeSpecifierType.IndexedKeys>; /** * {@link ShapeSpecifier} for {@link tupleShape}. * * @category Internal */ export type ShapeTuple<Parts extends Readonly<any[]>> = ShapeSpecifier<Parts, ShapeSpecifierType.Tuple>; /** * {@link ShapeSpecifier} for {@link or}. * * @category Internal */ export type ShapeOr<Parts extends AtLeastTuple<unknown, 1>> = ShapeSpecifier<Parts, ShapeSpecifierType.Or>; /** * {@link ShapeSpecifier} for {@link unknownShape}. * * @category Internal */ export type ShapeUnknown<Parts extends Readonly<[unknown]>> = ShapeSpecifier<Parts, ShapeSpecifierType.Unknown>; /** * {@link ShapeSpecifier} for {@link numericRange}. * * @category Internal */ export type ShapeNumericRange<T extends number = number> = ShapeSpecifier<[ T, T ], ShapeSpecifierType.NumericRange>; /** * {@link ShapeSpecifier} for {@link optional}. * * @category Internal */ export type ShapeOptional<T = unknown> = ShapeSpecifier<[T], ShapeSpecifierType.Optional>; /** * ======================================== * * Shape Functions * * ======================================== */ /** * Create a shape part that combines all of its inputs together with an intersection or "and". * * @category Shape Part * @example * * ```ts * import {and, defineShape} from 'object-shape-tester'; * * const myShape = defineShape({ * a: and({q: ''}, {r: -1}, {s: true}), * }); * * // `myShape.runtimeType` is `{a: {q: string, r: number, s: boolean}}` * ``` */ export declare function and<Parts extends AtLeastTuple<unknown, 1>>(...parts: Parts): ShapeAnd<Parts>; /** * Define a shape part that requires an instance of the given constructor. * * @category Shape Part * @example * * ```ts * import {classShape, defineShape} from 'object-shape-tester'; * * const myShape = defineShape({ * a: classShape(RegExp), * }); * * // `myShape.runtimeType` is `{a: RegExp}` * ``` */ export declare function classShape<Parts extends [AnyConstructor]>(...parts: Parts): ShapeClass<Parts>; /** * Define a shape part that requires an enum value. * * @category Shape Part * @example * * ```ts * import {enumShape, defineShape} from 'object-shape-tester'; * * enum MyEnum { * A = 'a', * B = 'b', * } * * const myShape = defineShape({ * a: enumShape(MyEnum), * }); * * // `myShape.runtimeType` is `{a: MyEnum}` * ``` */ export declare function enumShape<const Parts extends Readonly<[Record<string, number | string>, (number | string)?]>>(...parts: Parts): ShapeEnum<Parts>; /** * Define a shape part that requires _exactly_ the value given. * * @category Shape Part * @example * * ```ts * import {exact, defineShape} from 'object-shape-tester'; * * const myShape = defineShape({ * a: or(exact('hi'), exact('bye')), * }); * * // `myShape.runtimeType` is `{a: 'hi' | 'bye'}` * ``` */ export declare function exact<const Parts extends Readonly<AtLeastTuple<unknown, 1>>>(...parts: Parts): ShapeExact<Parts>; /** * Define a shape part that's an object with a specific set of keys and values. * * @category Shape Part * @example * * ```ts * import {exact, defineShape, indexedKeys} from 'object-shape-tester'; * * const myShape = defineShape({ * a: indexedKeys({ * keys: or(exact('hi'), exact('bye')), * values: { * helloThere: 0, * }, * required: false, * }), * }); * * // `myShape.runtimeType` is `{a: Partial<Record<'hi' | 'bye', {helloThere: number}>>}` * ``` */ export declare function indexedKeys<Parts extends Readonly<[BaseIndexedKeys]>>(...parts: Parts): ShapeIndexedKeys<Parts>; /** * Define a shape part requires a tuple. * * @category Shape Part * @example * * ```ts * import {exact, defineShape, tupleShape} from 'object-shape-tester'; * * const myShape = defineShape({ * a: tupleShape('a', -1, exact('hi')), * }); * * // `myShape.runtimeType` is `[string, number, 'hi']` * ``` */ export declare function tupleShape<Parts extends Readonly<any[]>>(...parts: Parts): ShapeTuple<Parts>; /** * Define a shape part that's a union of all its inputs. * * @category Shape Part * @example * * ```ts * import {or, defineShape} from 'object-shape-tester'; * * const myShape = defineShape({ * a: or('', -1), * }); * * // `myShape.runtimeType` is `{a: string | number}` * ``` */ export declare function or<Parts extends AtLeastTuple<unknown, 1>>(...parts: Parts): ShapeOr<Parts>; /** * Define a shape part that resolves simply to `unknown`. * * @category Shape Part * @example * * ```ts * import {unknownShape, defineShape} from 'object-shape-tester'; * * const myShape = defineShape({ * a: unknownShape, * }); * * // `myShape.runtimeType` is `{a: unknown}` * ``` */ export declare function unknownShape(defaultValue?: unknown): ShapeUnknown<[unknown]>; /** * Define a shape part that requires numbers to be within a specific range, inclusive. * * @category Shape Part * @example * * ```ts * import {numericRange, defineShape} from 'object-shape-tester'; * * const myShape = defineShape({ * // This will simply produce a type of `number` but will validate runtime values against the range. * a: numericRange(1, 10), * }); * // `myShape.runtimeType` is just `{a: number}` * * const myShape2 = defineShape({ * // If you want type safety, you must specify the allowed numbers manually * a: numericRange<1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10>(1, 10), * }); * // `myShape2.runtimeType` is `{a: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10}` * ``` */ export declare function numericRange<T extends number = number>(min: NoInfer<T>, max: NoInfer<T>): ShapeNumericRange<T>; /** * Define a shape part that is optional. This only makes sense as a property in an object. * * @category Shape Part * @example * * ```ts * import {optional, defineShape} from 'object-shape-tester'; * * const myShape = defineShape({ * a: optional(-1), * }); * * // `myShape.runtimeType` is `{a?: number}` * ``` */ export declare function optional<T>(part: T): ShapeOptional<T>; /** * ======================================== * * Shape Specifier Type Guards * * ======================================== */ /** * Checks if the input is an {@link and} shape specifier for internal type guarding purposes. * * @category Internal */ export declare function isAndShapeSpecifier(maybeSpecifier: unknown): maybeSpecifier is ShapeAnd<AtLeastTuple<unknown, 1>>; /** * Checks if the input is a {@link classShape} shape specifier for internal type guarding purposes. * * @category Internal */ export declare function isClassShapeSpecifier(maybeSpecifier: unknown): maybeSpecifier is ShapeClass<[AnyConstructor]>; /** * Checks if the input is an {@link enumShape} shape specifier for internal type guarding purposes. * * @category Internal */ export declare function isEnumShapeSpecifier(maybeSpecifier: unknown): maybeSpecifier is ShapeEnum<[Record<string, number | string>, (number | string)?]>; /** * Checks if the input is an {@link exact} shape specifier for internal type guarding purposes. * * @category Internal */ export declare function isExactShapeSpecifier(maybeSpecifier: unknown): maybeSpecifier is ShapeExact<[unknown]>; /** * Checks if the input is an {@link indexedKeys} shape specifier for internal type guarding purposes. * * @category Internal */ export declare function isIndexedKeysSpecifier(maybeSpecifier: unknown): maybeSpecifier is ShapeIndexedKeys<Readonly<[BaseIndexedKeys]>>; /** * Checks if the input is an {@link tupleShape} shape specifier for internal type guarding purposes. * * @category Internal */ export declare function isTupleShapeSpecifier(maybeSpecifier: unknown): maybeSpecifier is ShapeTuple<Readonly<any[]>>; /** * Checks if the input is an {@link or} shape specifier for internal type guarding purposes. * * @category Internal */ export declare function isOrShapeSpecifier(maybeSpecifier: unknown): maybeSpecifier is ShapeOr<AtLeastTuple<unknown, 1>>; /** * Checks if the input is an {@link unknownShape} shape specifier for internal type guarding * purposes. * * @category Internal */ export declare function isUnknownShapeSpecifier(maybeSpecifier: unknown): maybeSpecifier is ShapeUnknown<[unknown]>; /** * Checks if the input is a {@link numericRange} shape specifier for internal type guarding purposes. * * @category Internal */ export declare function isNumericRangeShapeSpecifier(maybeSpecifier: unknown): maybeSpecifier is ShapeNumericRange; /** * Checks if the input is a {@link optional} shape specifier for internal type guarding purposes. * * @category Internal */ export declare function isOptionalShapeSpecifier(maybeSpecifier: unknown): maybeSpecifier is ShapeOptional; /** * ======================================== * * Shape Value Run Time Type * * ======================================== */ /** @category Internal */ export type ExpandParts<Parts extends BaseParts, IsExact extends boolean, IsReadonly extends boolean> = Extract<ArrayElement<Parts>, ShapeDefinition<any, any>> extends never ? ShapeToRuntimeType<ArrayElement<Parts>, IsExact, IsReadonly> : ShapeToRuntimeType<Exclude<ArrayElement<Parts>, ShapeDefinition<any, any>>, IsExact, IsReadonly> | Extract<ArrayElement<Parts>, ShapeDefinition<any, any>>['runtimeType']; type MaybePartial<T, IsPartial extends boolean> = IsPartial extends true ? T : Partial<T>; /** @category Internal */ export type TupleParts<Parts extends ReadonlyArray<any>, IsExact extends boolean, IsReadonly extends boolean> = { [Index in keyof Parts]: SpecifierToRuntimeType<Parts[Index], IsExact, IsReadonly>; }; /** * Converts a shape specifier to a runtime type. * * @category Internal */ export type SpecifierToRuntimeType<PossiblySpecifier, IsExact extends boolean, IsReadonly extends boolean> = PossiblySpecifier extends ShapeSpecifier<infer Parts, infer Type> ? Type extends ShapeSpecifierType.NumericRange ? Parts[0] : Type extends ShapeSpecifierType.And ? OptionallyReadonly<IsReadonly, UnionToIntersection<ExpandParts<Parts, IsExact, IsReadonly>>> : Type extends ShapeSpecifierType.Class ? Parts[0] extends AnyConstructor ? OptionallyReadonly<IsReadonly, InstanceType<Parts[0]>> : 'TypeError: classShape input must be a constructor.' : Type extends ShapeSpecifierType.Or ? OptionallyReadonly<IsReadonly, ExpandParts<Parts, IsExact, IsReadonly>> : Type extends ShapeSpecifierType.Exact ? OptionallyReadonly<IsReadonly, WritableDeep<ExpandParts<Parts, true, IsReadonly>>> : Type extends ShapeSpecifierType.Enum ? OptionallyReadonly<IsReadonly, Parts[0][keyof Parts[0]]> : Type extends ShapeSpecifierType.IndexedKeys ? Parts[0] extends { keys: unknown; values: unknown; required: boolean; } ? ExpandParts<[ Parts[0]['keys'] ], IsExact, IsReadonly> extends PropertyKey ? OptionallyReadonly<IsReadonly, MaybePartial<Record<ExpandParts<[Parts[0]['keys']], IsExact, IsReadonly>, ExpandParts<[Parts[0]['values']], IsExact, IsReadonly>>, Parts[0]['required']>> : 'TypeError: indexedKeys keys be a subset of PropertyKey.' : 'TypeError: indexedKeys input is invalid.' : Type extends ShapeSpecifierType.Tuple ? TupleParts<Parts, IsExact, IsReadonly> : Type extends ShapeSpecifierType.Unknown ? unknown : Type extends ShapeSpecifierType.Optional ? ExpandParts<Parts, IsExact, IsReadonly> : 'TypeError: found no match for shape specifier type.' : PossiblySpecifier extends Primitive ? IsExact extends true ? PossiblySpecifier : PossiblySpecifier : PossiblySpecifier extends object ? PossiblySpecifier extends ShapeDefinition<any, any> ? PossiblySpecifier['runtimeType'] : OptionallyReadonly<IsReadonly, { [Prop in keyof PossiblySpecifier]: SpecifierToRuntimeType<PossiblySpecifier[Prop], IsExact, IsReadonly>; }> : PossiblySpecifier; /** @category Internal */ export type OptionallyReadonly<IsReadonly extends boolean, OriginalType> = IsReadonly extends true ? Readonly<OriginalType> : OriginalType; /** * Converts a shape definition to a runtime type. * * @category Util */ export type ShapeToRuntimeType<Shape, IsExact extends boolean, IsReadonly extends boolean> = Shape extends AnyFunction ? Shape : Shape extends CustomSpecifier<infer T> ? T : Shape extends object ? Shape extends ShapeDefinition<infer InnerShape, any> ? ShapeToRuntimeType<InnerShape, IsExact, IsReadonly> : Shape extends ShapeSpecifier<any, any> ? Shape extends ShapeSpecifier<any, ShapeSpecifierType.Exact> ? SpecifierToRuntimeType<Shape, true, IsReadonly> : SpecifierToRuntimeType<Shape, IsExact, IsReadonly> : Shape extends Array<any> ? OptionallyReadonly<IsReadonly, { [Prop in keyof Shape]: Shape[Prop] extends ShapeSpecifier<any, ShapeSpecifierType.Exact> ? ShapeToRuntimeType<Shape[Prop], true, IsReadonly> : ShapeToRuntimeType<Shape[Prop], IsExact, IsReadonly>; }> : OptionallyReadonly<IsReadonly, Simplify<{ [Prop in keyof Shape as Shape[Prop] extends ShapeOptional<any> ? never : Prop]: Shape[Prop] extends ShapeOptional<any> ? never : Shape[Prop] extends ShapeSpecifier<any, ShapeSpecifierType.Exact> ? ShapeToRuntimeType<Shape[Prop], true, IsReadonly> : ShapeToRuntimeType<Shape[Prop], IsExact, IsReadonly>; } & { [Prop in keyof Shape as Shape[Prop] extends ShapeOptional<any> ? Prop : never]?: Shape[Prop] extends ShapeOptional<any> ? Shape[Prop] extends ShapeSpecifier<any, ShapeSpecifierType.Exact> ? ShapeToRuntimeType<Shape[Prop], true, IsReadonly> : ShapeToRuntimeType<Shape[Prop], IsExact, IsReadonly> : never; }>> : IsEqual<IsExact, true> extends true ? Shape : IsNever<LiteralToPrimitive<Shape>> extends true ? Shape : LiteralToPrimitive<Shape>; /** * If the input is a shape specifier, return it type guarded as such. * * @category Internal * @returns `undefined` if the input is not */ export declare function getShapeSpecifier(input: unknown): ShapeSpecifier<BaseParts, ShapeSpecifierType> | undefined; export {};