
Refine Properties on Existing TypeScript Types
Lift a check on one child value back onto its parent, then reuse the resulting predicate in branches, filter, find, and deeper compositions.
The quick answer
Use refineKey when the parent is already typed and a required property needs a more precise reusable predicate.
import { isString, refineKey } from 'is-kit';type Item = {readonly id: number;readonly value: string | number;};const hasStringValue = refineKey('value', isString);declare const items: readonly Item[];const textItems = items.filter(hasStringValue);// Array<Item & Record<'value', string>>textItems[0]?.value.toUpperCase();
The result preserves both facts: the value is still an Item, and its value property is a string. The named predicate carries that intersection through filter without a handwritten predicate annotation.
Property refinement is useful when the parent type is already known. It is not a replacement for validating an unknown object shape.
Why lift the child check onto its parent
TypeScript narrows a property inside a local branch, but an extracted compound condition does not generally advertise the corresponding parent-property intersection as its return type.
import { isString, refineKey } from 'is-kit';type Item = { readonly value: string | number };declare const items: readonly Item[];const checksStringValue = (item: Item) => isString(item.value);const checked = items.filter(checksStringValue);// Item[]: the extracted function returns booleanconst hasStringValue = refineKey('value', isString);const refined = items.filter(hasStringValue);// Array<Item & Record<'value', string>>
refineKey turns the same runtime check into a reusable type predicate. This is the core capability; it applies to ordinary application models, library types, generated clients, and ASTs alike.
Choose the property contract explicitly
| Helper | Use it for | Returns false before refinement |
|---|---|---|
| refineKey | One required property | Never; the property belongs to the input contract |
| refineDefinedKey | One optional property that must be defined | The value is missing or undefined |
| refineIndex | One readonly-array element | The index is absent, sparse, inherited, or undefined |
Absence is runtime behavior, not only a type annotation. Separate helpers make it visible whether a missing child violates the input contract or simply makes the predicate fail.
Refine one required property
refineKey reads a required property once, applies the supplied refinement once, and preserves every unrelated part of the parent type.
import { isString, refineKey } from 'is-kit';type Message = {readonly id: string;readonly body: string | Uint8Array;};const hasTextBody = refineKey('body', isString);declare const message: Message;if (hasTextBody(message)) {message.body.toUpperCase(); // body: stringmessage.id.toUpperCase(); // unrelated fields remain available}
It uses normal property access, so inherited properties and accessors follow ordinary JavaScript behavior.
Require and refine an optional property
Use refineDefinedKey when absence is allowed by the input type but should make this particular predicate return false.
import { isString, refineDefinedKey } from 'is-kit';type Job = {readonly id: string;readonly result?: string | Uint8Array;};const hasTextResult = refineDefinedKey('result', isString);declare const jobs: readonly Job[];const completedTextJobs = jobs.filter(hasTextResult);// Array<Job & Record<'result', string>>
A missing or explicitly undefined result is not passed to isString. A successful check records the property as required and narrowed on the parent.
Refine one array element
Use refineIndex when one concrete array position must exist and satisfy another refinement. Lift it with refineKey when the array belongs to a parent object.
import { isString, refineIndex, refineKey } from 'is-kit';type Batch = {readonly values: readonly (string | number)[];};const startsWithString = refineKey('values',refineIndex(0, isString),);declare const batch: Batch;if (startsWithString(batch)) {batch.values[0].toUpperCase(); // index 0 exists and is string}
The own-element check rejects out-of-bounds access, sparse holes, inherited numeric properties, and explicit undefined, including when noUncheckedIndexedAccess is disabled.
Compose nested and literal refinements
Property refinements remain ordinary predicates. Compose them one property at a time with andAll, and use equals when the child should narrow to a literal value.
import {andAll,equals,isString,refineKey,} from 'is-kit';type TextPayload = {readonly kind: 'text';readonly status: 'pending' | 'ready';readonly body: string | Uint8Array;};type Payload =| TextPayload| { readonly kind: 'binary'; readonly bytes: Uint8Array };type Envelope = { readonly payload: Payload };const isTextPayload = (payload: Payload): payload is TextPayload =>payload.kind === 'text';const hasReadyTextPayload = refineKey('payload',andAll(isTextPayload,refineKey('status', equals('ready')),refineKey('body', isString),),);declare const envelope: Envelope;if (hasReadyTextPayload(envelope)) {envelope.payload.status; // 'ready'envelope.payload.body.toUpperCase(); // string}
Each successful check remains visible on the final parent type. A concrete child guard establishes the input domain, the following property refinements accumulate facts about that child, and the outer refineKey carries the result back to the parent.
Know when a local predicate is enough
A handwritten predicate remains a clear dependency-free alternative, especially for one local shape. The tradeoff is writing and maintaining the parent intersection yourself.
type Item = { readonly value: string | number };type ItemWithStringValue = Item & { readonly value: string };const hasStringValue = (item: Item): item is ItemWithStringValue =>typeof item.value === 'string';
Use the helpers when the pattern repeats, composes, or benefits from a shared type vocabulary. Keep a one-off local branch inline when it is already the clearest expression.
Advanced example: Compiler API nodes
The TypeScript Compiler API is a demanding application of the same generic pattern: broad nodes first narrow with an isX guard, then child properties need reusable refinements of their own.
Continue with Advanced property refinement with the TypeScript Compiler API for required, optional, indexed, and nested AST examples.
Summary
- Use
refineKeyfor one required property. - Use
refineDefinedKeywhen an optional property must be present and defined. - Use
refineIndexfor one defined own array element. - Compose helpers to retain nested and literal child facts.
- Validate unknown object shapes with a guard such as struct first.
See the key API reference for the complete contracts and key-domain restrictions.