Skip to content

v1.13.0

Choose a tag to compare

@nyaomaru nyaomaru released this 22 Aug 04:49
· 132 commits to main since this release
Immutable release. Only release title and notes can be modified.
43c7358

What's new 🚀

Compose refinements while preserving their input domain

and, andAll, or, and oneOf now preserve the original input domain when composing refinements.

Previously, composing type predicates could widen the resulting function back to a guard accepting unknown. The returned predicate now remains a Refine<A, B>, retaining both its known input type and final narrowed output.

import { and, andAll, oneOf, or } from 'is-kit';
import type { Refine } from 'is-kit';

type AstNode =
  | { kind: 'identifier'; text: string }
  | { kind: 'literal'; value: string }
  | { kind: 'call'; expression: AstNode };

type Identifier = Extract<AstNode, { kind: 'identifier' }>;
type Literal = Extract<AstNode, { kind: 'literal' }>;
type Call = Extract<AstNode, { kind: 'call' }>;

type IdentifierCall = Call & {
  expression: Identifier;
};

type NamedIdentifierCall = IdentifierCall & {
  expression: Identifier & { text: 'run' };
};

const isIdentifier = (node: AstNode): node is Identifier =>
  node.kind === 'identifier';

const isLiteral = (node: AstNode): node is Literal =>
  node.kind === 'literal';

const hasIdentifierExpression = (
  node: Call
): node is IdentifierCall => isIdentifier(node.expression);

const hasRunExpression = (
  node: IdentifierCall
): node is NamedIdentifierCall => node.expression.text === 'run';

const identifierCall = and(isCall, hasIdentifierExpression);
// Refine<AstNode, IdentifierCall>

const namedIdentifierCall = andAll(
  isCall,
  hasIdentifierExpression,
  hasRunExpression
);
// Refine<AstNode, NamedIdentifierCall>

const identifierOrLiteral = or(isIdentifier, isLiteral);
// Refine<AstNode, Identifier | Literal>

const leafNode = oneOf(isIdentifier, isLiteral);
// Refine<AstNode, Identifier | Literal>

The refinements can be reused directly with values whose domain is already known:

declare const node: AstNode;

if (namedIdentifierCall(node)) {
  node.expression.text;
  // 'run'
}

Forward generic refinement chains to andAll

Higher-order helpers can now forward generically constrained refinements without losing their narrowing.

This includes:

  • generically constrained individual refinements;
  • homogeneous refinement arrays and rest tuples;
  • progressive heterogeneous refinement chains;
  • concrete tuples whose refinements accept a broader input domain.
import { andAll } from 'is-kit';
import type { Refine } from 'is-kit';

function composeChain<
  A,
  B extends A,
  C extends B,
  D extends C,
  E extends D,
  F extends Refine<A, B>,
  Steps extends [
    Refine<B, C>,
    Refine<C, D>,
    Refine<D, E>
  ]
>(precondition: F, steps: Steps) {
  return andAll(precondition, ...steps);
  // Refine<A, E>
}

For a generic readonly tuple, pass the tuple directly instead of spreading it:

function composeReadonlyChain<
  A,
  B extends A,
  C extends B,
  D extends C,
  E extends D,
  F extends Refine<A, B>,
  Steps extends readonly [
    Refine<B, C>,
    Refine<C, D>,
    Refine<D, E>
  ]
>(precondition: F, steps: Steps) {
  return andAll(precondition, steps);
  // Refine<A, E>
}

Existing spread calls remain supported:

const namedIdentifierCall = andAll(
  isCall,
  hasIdentifierExpression,
  hasRunExpression
);

Safer refinement constraints

Refinement combinators now reject ordinary boolean-returning functions where a type predicate is required.

andAll(isPositive);

This prevents a boolean callback from being treated as proof of type narrowing. Invalid refinement chains whose output does not extend the preceding input are also rejected.

Improved public refinement types

Predicate<T> is now defined in terms of Refinement<unknown, T>, establishing Refinement<A, B> as the common representation for both unknown-input guards and known-domain refinements.

GuardedOf<F> can now extract the narrowed output from either form.

import type {
  GuardedOf,
  Predicate,
  Refinement
} from 'is-kit';

type StringPredicate = Predicate<string>;
type StringRefinement = Refinement<string | number, string>;

type PredicateOutput = GuardedOf<StringPredicate>;
// string

type RefinementOutput = GuardedOf<StringRefinement>;
// string

📄 Documentation improvements

The documentation is now available at is-kit.dev.

What's Changed

  • docs(changelog): 1.12.1 by @github-actions[bot] in #269
  • docs: add guide for keeping type guards in sync by @nyaomaru in #270
  • docs: add guide for validating unknown values by @nyaomaru in #271
  • docs: migrate documentation URLs to is-kit.dev by @nyaomaru in #272
  • chore(docs): use PNG image for social previews by @nyaomaru in #273
  • feat: support known-domain refinement composition by @nyaomaru in #274
  • refactor(docs): extract shared guide components by @nyaomaru in #275
  • Release: 1.13.0 by @github-actions[bot] in #276

Full Changelog: v1.12.1...v1.13.0