npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

zod-assignability

v0.0.2

Published

TypeScript extend behavior implementation for Zod schemas

Readme

Zod Assignability

Check if one Zod schema can be safely used where another Zod schema is expected. Think of it as a runtime analogue of TypeScript’s "A extends B" (assignability) relation, applied to Zod v4 schemas.

Assignability, Explained

In TypeScript, a type A is assignable to type B if every value that fits A is also valid for B. Common mental models:

  • Literals and enums: narrower sets extend broader sets ('a' -> string, 'a' | 'b' -> 'a' | 'b' | 'c').
  • Objects: a source must include at least the properties the target requires, with compatible property types; extra source properties are fine.
  • Arrays: element types are covariant (string[] -> (string | number)[]).
  • Tuples: same length, and each position must be assignable.
  • Unions: a source union extends a target only if every option in the source can be assigned to the target.
  • Intersections: a source intersection extends a target only if both sides do.
  • Special types: never extends everything; everything extends unknown and any (conservatively).

This library brings those rules to Zod schemas at runtime.

Installation

  • pnpm add zod-assignability zod
  • Requires zod@^4.

If you’re working locally in this repo, you can import from the source:

import { isAssignable } from './src/assignability';

Otherwise, after publishing/installing:

import { isAssignable } from 'zod-assignability';

API

  • isAssignable(source: SomeType, target: SomeType): boolean
    • Returns true if source is assignable to target under the conservative rules described below.
    • SomeType refers to Zod v4 core types (import type { SomeType } from 'zod/v4/core').

Quick Start

import { z } from 'zod';
import { isAssignable } from 'zod-assignability';

// Primitives
isAssignable(z.string(), z.string()); // true
isAssignable(z.string(), z.number()); // false

// Literals and unions
isAssignable(z.literal('a'), z.string()); // true
isAssignable(
  z.union([z.literal('a'), z.literal('b')]),
  z.union([z.literal('a'), z.literal('b'), z.literal('c')]),
); // true

// Objects
const A = z.object({ name: z.string() });
const B = z.object({ name: z.string(), age: z.number() });
isAssignable(A, B); // false (B requires age)
isAssignable(B, A); // true (extra props are okay)

// Optional target property
const C = z.object({ name: z.string(), age: z.number().optional() });
isAssignable(B, C); // true
isAssignable(A, C); // true (A is assignable because C's 'age' is optional)

Detailed Rules & Examples

  • Unknown, Any, Never

    • A -> unknown: always true.
    • A -> any: treated as true.
    • any -> B: treated as true.
    • never -> B: always true.
    • unknown -> B (when B is not unknown): false.
  • Optional / Nullable wrappers

    • Target optional/nullable: matching the inner type or the wrapper value is allowed.
      isAssignable(z.string(), z.string().optional()); // true
      isAssignable(z.null(), z.string().nullable()); // true
    • Source optional/nullable: behaves like unions for variance.
      isAssignable(z.string().optional(), z.string()); // false (needs `string` and `undefined` -> `string`)
      isAssignable(z.string().nullable(), z.union([z.string(), z.null()])); // true
  • Literals and Enums

    • Literal to literal: values must match.
    • Literal to primitive: allowed if the literal’s value is of that primitive.
    • Literal to union: literal must match at least one union member.
    • Enum to enum: source values must be a subset of target values.
    • Enum to primitive: allowed when every enum member is of that primitive.
      const E1 = z.enum(['A', 'B']);
      const E2 = z.enum(['A', 'B', 'C']);
      isAssignable(E1, E2); // true
      isAssignable(E1, z.string()); // true
  • Primitives

    • Primitive to primitive: assignable only when types are identical.
      isAssignable(z.boolean(), z.boolean()); // true
      isAssignable(z.boolean(), z.string()); // false
  • Arrays

    • Element types are covariant.
      isAssignable(
        z.array(z.string()),
        z.array(z.union([z.string(), z.number()])),
      ); // true
      isAssignable(z.array(z.number()), z.array(z.string())); // false
  • Tuples

    • Invariant length; element-wise assignability.
      isAssignable(
        z.tuple([z.string(), z.number()]),
        z.tuple([z.string(), z.number()]),
      ); // true
      isAssignable(z.tuple([z.string(), z.number()]), z.tuple([z.string()])); // false
  • Objects (structural)

    • Every required key in target must exist in source.
    • If target requires a key, source must also require it.
    • Property types are covariant; extra properties in source are allowed.
      const S = z.object({ name: z.string(), age: z.number() });
      const T = z.object({ name: z.string() });
      isAssignable(S, T); // true
      isAssignable(T, S); // false
  • Records

    • Key types and value types must be assignable.
      const R1 = z.record(z.string(), z.string());
      const R2 = z.record(z.string(), z.union([z.string(), z.number()]));
      isAssignable(R1, R2); // true
  • Unions

    • Source union to target: every source option must be assignable to target.
    • Target union: source is assignable if it’s assignable to at least one member of the target.
      const Uab = z.union([z.literal('a'), z.literal('b')]);
      const Uabc = z.union([z.literal('a'), z.literal('b'), z.literal('c')]);
      isAssignable(Uab, Uabc); // true
      isAssignable(Uabc, Uab); // false
  • Intersections

    • Source intersection: assignable if the combined source (for objects) or at least one of its components is assignable to the target.
    • Target intersection: source must be assignable to both sides.
  • Custom / Instanceof

    • Only identical custom schemas are considered assignable (conservative).
      const Dog = z.instanceof(class Dog {});
      const Animal = z.instanceof(class Animal {});
      isAssignable(Dog, Dog); // true
      isAssignable(Dog, Animal); // false

Notes and Caveats

  • This library inspects Zod v4 core internals (via schema._zod.def). If Zod’s internals change, helpers may need updates.
  • Optional property detection in objects also treats Union<..., undefined> as optional.
  • Nullable detection focuses on the wrapper form (z.string().nullable()); unions with null are not unwrapped as a nullable wrapper.
  • Excess property checks are not enforced; extra source properties are allowed.
  • Instanceof/custom schemas are treated conservatively; only identical schemas are assignable.

Development

  • Build: pnpm build
  • Test: pnpm test