CruftScript utility types
Documents CruftScript's TypeScript-style utility types: Partial, Required, Pick, Omit, Record, Exclude, ReturnType, Awaited, and the rest. Each is a build-time computation erased before the program runs, and each entry gives its definition, behavior, how it differs from TypeScript, and whether it is implemented yet.
CruftScript provides the familiar TypeScript utility types, but every one of them is a build-time (T0) type computation that is fully resolved by the checker and erased before the program runs. None has any runtime representation; what crosses a boundary is the concrete shape a utility resolves to, validated on that shape. This page documents each utility exhaustively: its definition, exact behavior, the ways CruftScript's version differs from TypeScript's, worked examples, and edge cases.
For the language as a whole see the CruftScript reference; the utilities compose the type system's mapped types, conditional types, infer, and keyof, and add no new mechanism.
The rules they all obey
Four principles run through every entry below, so they are stated once here:
- No
any.unknownis the only bottom type. Where TypeScript falls back toany(as inReturnTypeof a non-function), CruftScript yieldsunknownor a build-time error. - An undecidable operand is a build-time error, not a silent fallback. When a utility cannot decide its operand (a non-function passed to
Parameters, an unresolvable union member), the checker refuses at build time with a diagnostic rather than guessing a type. This is the soundness-over-convenience stance, and it is the single most common way these utilities diverge from TypeScript. keyof unknownisnever. Because there is noany, a utility whose keys derive fromunknownmaps over an empty key set and resolves to{}rather than exploding.- Homomorphic mappings preserve modifiers. A mapping over
keyof Tcarries each property'sreadonlyand optional modifiers through; an explicit modifier always overrides the inherited one.
Every utility is erased at T0. Except where noted, each is implemented in the checker today.
Object mappers
These map over an object type's keys, preserving modifiers homomorphically.
Partial<T>
Makes every property of T optional, keeping each property's readonly modifier and value type.
type Partial<T> = { [K in keyof T]+?: T[K] }
Behavior. For each key of T, the property's value type is preserved and the optional modifier is added. Because the mapping is homomorphic, a readonly property stays readonly; an already-optional property stays optional.
Partial<{ id: number; name: string }> => { id?: number; name?: string }
Partial<{ a: number; readonly b: string; c?: boolean }>
=> { a?: number; readonly b?: string; c?: boolean }
Partial<unknown> => {}
Edge cases. Partial<{}> is {}. It distributes over a union (Partial<A | B> is Partial<A> | Partial<B>) and is idempotent (Partial<Partial<T>> equals Partial<T>).
Required<T>
The inverse of Partial: removes the optional modifier from every property, making all required, while preserving readonly and value types.
type Required<T> = { [K in keyof T]-?: T[K] }
Behavior. Only the optional modifier is touched; the value type is copied verbatim (a union member undefined in the value is not additionally removed, matching TypeScript). Over a concrete object the checker rewrites it directly; over a still-generic T it stays a mapped type until T is known.
Required<{ a?: number; b: string }> => { a: number; b: string }
Required<{ readonly x?: boolean }> => { readonly x: boolean }
Required<{ a?: number } | { b?: string }> => { a: number } | { b: string }
Edge cases. Required<{}> is {}; Required<unknown> is {}; it distributes over unions and is idempotent.
Readonly<T>
Marks every property readonly, preserving keys, value types, and optionality.
type Readonly<T> = { readonly [K in keyof T]: T[K] }
Behavior. readonly is added to every property; optional flags are untouched. The mapping is shallow: a nested object's properties are not recursively made readonly. readonly is a type-level modifier only; it carries no runtime immutability enforcement, so the erased runtime value is an ordinary object.
Readonly<{ readonly id: number; name?: string }> => { readonly id: number; readonly name?: string }
Readonly<unknown> => {}
Edge cases. Idempotent; distributes over unions; a wrong argument count is a build-time error (Readonly expects exactly one type argument).
Record<K, V>
Builds an object type whose keys are the union K and whose every value is V.
type Record<K, V> = { [P in K]: V }
Behavior. Unlike the mappers above, Record is not homomorphic, it maps over an explicit key union rather than keyof T, so no modifiers are inherited: every produced property is required and mutable. The checker resolves it three ways by what K is:
Kis a named key, or a union of named keys → a concrete object with one property per key (duplicates collapse).Kis exactlystring→ the runtime string-keyed record type (missing-key access throws). This is the one utility that lands on a runtime construct rather than a purely erased shape.Kis anything else → a build-time error.
Record<"a" | "b", number> => { a: number; b: number }
Record<string, boolean> => a string-keyed record of boolean
Record<number, string> => build-time error (numeric index keys are not admitted)
Edge cases. A key union deriving from unknown is never, giving {}. Numeric index keys, which TypeScript accepts, are currently a build-time error. Requires exactly two type arguments.
Key selection
These select or drop named properties. Both enforce that the named keys actually exist on T, a real constraint, not a nominal annotation.
Pick<T, K>
Keeps only the properties of T whose names are in K.
type Pick<T, K extends keyof T> = { [P in K]: T[P] }
Behavior. T must be a decidable object type and K a named key or union of named keys. Each selected property is copied verbatim, name, value type, readonly, and optional modifiers, in the order K requests. Duplicate keys collapse.
type U = { readonly id: number; name?: string; age: number }
Pick<U, "id"> => { readonly id: number }
Pick<U, "id" | "name"> => { id: number; name?: string }
Edge cases. Requesting a key not present on T is specified to be a build-time error under the K extends keyof T bound, but that bound is not yet enforced: Pick<U, "absent"> currently checks clean (exit 0). A non-object T, or K being string/number (index-signature domains rather than named keys), is a build-time error.
Omit<T, K>
The complement of Pick: every property of T whose key is not in K.
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>
Behavior. Same requirements as Pick; surviving properties keep their modifiers homomorphically.
type User = { readonly id: number; name: string; age?: number }
Omit<User, "age"> => { readonly id: number; name: string }
Omit<User, "name" | "age"> => { readonly id: number }
Edge cases. This is the sharpest specified divergence from TypeScript: omitting a key not present on T is meant to be a build-time error, where TypeScript silently returns the type unchanged (its K is typed loosely as keyof any precisely to allow that). That rejection is not yet enforced, however — Omit<U, "absent"> currently checks clean (exit 0), same root as Pick. Omitting every property yields {}. Omit does not distribute over a union target.
Union filters
These filter the members of a union. All three share one engine and one normalization: zero survivors collapse to never, a single survivor collapses to that type bare (not a one-member union).
Exclude<T, U>
Removes from union T every member assignable to U.
type Exclude<T, U> = T extends U ? never : T
Exclude<"a" | "b" | "c", "a"> => "b" | "c"
Exclude<string | number, string> => number
Exclude<"x", "x" | "y"> => never
Behavior & edge cases. T is distributed member by member; a non-union T is treated as a single member. If a member's assignability to U cannot be decided (for instance an object-shape member against an opaque filter), the checker raises a build-time error rather than guessing, TypeScript would silently defer. Requires exactly two type arguments.
Extract<T, U>
The complement of Exclude: keeps the members of T assignable to U.
type Extract<T, U> = T extends U ? T : never
Extract<"a" | "b" | "c", "a" | "f"> => "a"
Extract<string | number | boolean, string | number> => string | number
Extract<"x" | "y", "a" | "b"> => never
Behavior & edge cases. Extract<T, unknown> keeps every member (everything is assignable to unknown). An undecidable member is a build-time error, not a deferred conditional.
NonNullable<T>
Removes null and undefined from T.
type NonNullable<T> = T extends null | undefined ? never : T
Behavior. Exactly the Exclude engine with the filter fixed to null | undefined. It is the sound type-level replacement for the ! non-null assertion: it expresses "this is not null" in the type system without an unchecked assertion. Typing a value NonNullable<T> does not make a boundary-crossed value non-null at runtime; a nullable value arriving across a boundary still requires real narrowing before use.
NonNullable<string | null | undefined> => string
NonNullable<null | number> => number
NonNullable<null | undefined> => never
Edge cases. NonNullable<null> and NonNullable<undefined> are never. NonNullable<unknown> is specified to be a build-time error (nullishness is undecidable), but that check is not yet enforced — it currently checks clean (exit 0).
Function and constructor extraction
These use a conditional with an infer to read a piece of a function or constructor type. A non-callable operand is specified to be a build-time error, not TypeScript's silent never/any — though that guard is not yet enforced: Parameters<number> currently checks clean (exit 0).
Parameters<T>
The tuple of a function type's parameter types.
type Parameters<T> = T extends (...a: infer P) => unknown ? P : never
Behavior. Collects each parameter type into a tuple in order; a this pseudo-parameter is excluded; a zero-parameter function gives []. Distributes over a union of function types.
Parameters<(input: string, count: number) => boolean> => [string, number]
Parameters<(this: User, input: string) => boolean> => [string]
Parameters<((a: string) => void) | ((a: number, b: boolean) => void)>
=> [string] | [number, boolean]
Edge cases. A non-function operand is specified to be a build-time error, but the non-callable guard is not yet enforced — Parameters<number> currently checks clean (exit 0). In a union, one non-function member is meant to fail the whole expansion (fail-closed), rather than producing a partial union.
ReturnType<T>
A function type's return type.
type ReturnType<T> = T extends (...a: infer _) => infer R ? R : unknown
Behavior. Captures the return position; parameters are discarded. Overloaded types resolve against the last (most specific) signature. Distributes over unions.
ReturnType<() => string> => string
ReturnType<(x: number, y: number) => { sum: number }> => { sum: number }
ReturnType<(() => string) | (() => number)> => string | number
Edge cases. TypeScript's fallback here is any; CruftScript yields unknown in an aliasing position, and in an active inference context where the parameter cannot be resolved it is instead a build-time error demanding an explicit type argument, never a silent widen.
ConstructorParameters<T>
The parameter tuple of a constructor type.
type ConstructorParameters<T> = T extends abstract new (...a: infer P) => unknown ? P : never
Behavior. Reads a constructor type's parameters into a tuple; both abstract and concrete constructors qualify; a zero-parameter constructor gives []. Distributes over a union of constructor types.
ConstructorParameters<new (x: number, y: number) => Point> => [number, number]
ConstructorParameters<new (label: string) => Widget> => [string]
ConstructorParameters<new () => Empty> => []
Edge cases. A non-constructor operand is specified to be a build-time error (not TypeScript's silent never), though that guard is not yet enforced: ConstructorParameters<number> currently checks clean (exit 0). In a union, one non-constructor member fails the whole expansion. Requires exactly one type argument.
InstanceType<T>
The instance type a constructor produces, the type of new T(...).
type InstanceType<T> = T extends abstract new (...a: infer _) => infer R ? R : unknown
Behavior. Returns the constructor's instance type; abstract and concrete constructors both match; distributes over unions.
InstanceType<typeof Point> => Point
InstanceType<abstract new (n: number) => { id: string }> => { id: string }
Edge cases. As with ReturnType, the TypeScript any fallback becomes unknown at the alias level. A non-constructor operand is specified to tighten to a build-time error rather than being accepted, though that guard is not yet enforced: InstanceType<number> currently checks clean (exit 0).
The this parameter
ThisParameterType<T>
The declared type of a function type's explicit this parameter.
type ThisParameterType<T> = T extends (this: infer U, ...a: never) => unknown ? U : unknown
Behavior. Returns the this parameter's type; a function with no explicit this yields unknown; distributes over unions.
ThisParameterType<(this: Document, ev: Event) => void> => Document
ThisParameterType<(x: number) => number> => unknown
Edge cases. A non-function operand (including an empty object type {}) is specified to be a build-time error, where TypeScript returns unknown — though that guard is not yet enforced: ThisParameterType<{}> currently checks clean (exit 0).
OmitThisParameter<T>
A function type with its explicit this parameter stripped.
type OmitThisParameter<T> = T extends (this: infer _, ...a: infer A) => infer R ? (...a: A) => R : T
Behavior. Rebuilds the same call signature without the this parameter, preserving the other parameters, the return type, and any boundary annotation. A function with no explicit this is returned unchanged. Distributes over unions.
OmitThisParameter<(this: Grid, x: number) => string> => (x: number) => string
OmitThisParameter<(a: number) => void> => (a: number) => void
Edge cases. A non-function operand is specified to be a build-time error, where TypeScript returns the operand unchanged — though that guard is not yet enforced: OmitThisParameter<number> currently checks clean (exit 0). In a union, one non-function member fails the whole expansion.
Async
Awaited<T>
Recursively strips nested Promise layers to yield the settled value type.
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T
Behavior. Each step unwraps exactly one Promise layer, so a deeply nested promise collapses to its first non-promise type. Non-promise types are returned unchanged. Distributes over unions.
Awaited<Promise<Promise<string>>> => string
Awaited<string> => string
Awaited<Promise<string> | number> => string | number
Soundness. Awaited is the one recursive utility, and its recursion is governed by the structural-decrease termination rule: each unwrap provably removes one layer, so reduction always terminates; a depth backstop errors rather than looping. An unknown (or otherwise undecidable) operand is specified to be a build-time error, not an any fallback — though that guard is not yet enforced: Awaited<unknown> currently checks clean (exit 0).
Edge cases. It recognizes only the built-in Promise<T> shape; an arbitrary { then(...) } thenable object type is returned unchanged (a deliberate narrowing from TypeScript). It is the type-level face of await; the runtime await mechanism is a separate concern.
Inference control
NoInfer<T>
Structurally the identity on T, but blocks the wrapped position from contributing inference candidates for T's type parameters.
type NoInfer<T> = T // identity in shape; steers inference only
Behavior. NoInfer<T> resolves to exactly T, no widening, narrowing, or distribution. Its only effect is on inference: a type parameter mentioned inside a NoInfer<...> position collects no candidate there, so inference must be driven by the other positions, and the NoInfer position is then checked against the result.
declare function f<T>(primary: T, guard: NoInfer<T>): T
f("x", "y") // T is inferred from `primary` only (= "x"); "y" is checked against it
Soundness. NoInfer only restricts inference sources, so it can never introduce an unsound type. If a type parameter is mentioned only inside NoInfer positions, its candidate set is empty and the checker raises the inference-failure build-time error demanding an explicit type argument, never a silent fallback.
Status. NoInfer is defined by the spec but is not yet implemented in the checker; the inference collector does not currently honor the suppression. It is documented here for completeness; the other sixteen utilities are live today.