Skip to main content

Essential TypeScript Utility Packages for 2026

·PkgPulse Team
0

TypeScript's type system is powerful, but some tasks need libraries to be practical. These utility packages fill the gaps — from runtime validation to type manipulation to developer ergonomics.

Every package here is actively maintained, well-typed, and used in production by thousands of projects. Download data from PkgPulse.

Schema Validation

Zod

The TypeScript-first schema validation library. Define a schema once, get both runtime validation and static types.

  • Downloads: 14M/week
  • Size: 13KB (gzip)
  • Use case: Form validation, API input/output, config parsing
import { z } from 'zod';

const UserSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
  age: z.number().int().positive().optional(),
});

type User = z.infer<typeof UserSchema>; // TypeScript type auto-generated

const result = UserSchema.safeParse(input);
if (result.success) {
  console.log(result.data); // Fully typed
}

Why it dominates: One schema definition gives you both TypeScript types and runtime validation. No more writing types AND validation rules separately.

Valibot

The lightweight alternative to Zod. Modular, tree-shakeable, 98% smaller in many cases.

  • Downloads: 400K/week
  • Size: 0.5-5KB (depending on features used)
  • Use case: Same as Zod, but when bundle size is critical
import * as v from 'valibot';

const UserSchema = v.object({
  name: v.pipe(v.string(), v.minLength(2)),
  email: v.pipe(v.string(), v.email()),
  age: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1))),
});

type User = v.InferOutput<typeof UserSchema>;

When to choose over Zod: Client-side validation where bundle size matters. Valibot's tree-shaking means you only ship the validators you use.

Type Utilities

ts-reset

Fixes TypeScript's built-in types to be more useful. A single import that makes .filter(Boolean), .json(), and JSON.parse() return better types.

  • Downloads: 700K/week
  • Size: 0KB (types only, no runtime)
// Before ts-reset
const filtered = [1, undefined, 2].filter(Boolean);
// Type: (number | undefined)[] — still includes undefined!

// After ts-reset
const filtered = [1, undefined, 2].filter(Boolean);
// Type: number[] — correct!
// Before ts-reset
const data = JSON.parse(input);
// Type: any

// After ts-reset
const data = JSON.parse(input);
// Type: unknown — forces you to validate

Install it on every TypeScript project. Zero runtime cost, immediate type safety improvements.

type-fest

A curated collection of essential TypeScript utility types. If you've ever needed PartialDeep, SetRequired, CamelCase, or Simplify — it's here.

  • Downloads: 30M/week
  • Size: 0KB (types only)
import type { PartialDeep, CamelCase, SetRequired } from 'type-fest';

// Make all nested properties optional
type DeepPartialUser = PartialDeep<User>;

// Convert string to camelCase type
type Key = CamelCase<'user-profile-settings'>; // 'userProfileSettings'

// Make specific optional fields required
type UserWithEmail = SetRequired<User, 'email'>;

ts-pattern

Exhaustive pattern matching for TypeScript. Like a supercharged switch statement that the compiler verifies is complete.

  • Downloads: 1M/week
  • Size: 3KB (gzip)
import { match, P } from 'ts-pattern';

type Shape =
  | { type: 'circle'; radius: number }
  | { type: 'rect'; width: number; height: number };

const area = match(shape)
  .with({ type: 'circle' }, ({ radius }) => Math.PI * radius ** 2)
  .with({ type: 'rect' }, ({ width, height }) => width * height)
  .exhaustive(); // Compile error if a case is missing

Date & Time

date-fns

Modular date utility library. Import only the functions you need — tree-shaking keeps your bundle small.

  • Downloads: 18M/week
  • Size: 2-10KB (per function, gzipped)
import { format, addDays, differenceInDays } from 'date-fns';

format(new Date(), 'yyyy-MM-dd'); // '2026-03-04'
addDays(new Date(), 7); // Next week
differenceInDays(endDate, startDate); // Days between

Day.js

Moment.js replacement with the same API but at 2KB. If you want a simple, familiar API:

  • Downloads: 16M/week
  • Size: 2.9KB (gzip)
import dayjs from 'dayjs';

dayjs().format('YYYY-MM-DD');
dayjs().add(7, 'day');
dayjs(endDate).diff(startDate, 'day');

Environment & Configuration

dotenv

Load environment variables from .env files. Still the standard after all these years.

  • Downloads: 35M/week
  • Size: 2KB

@t3-oss/env-nextjs

Type-safe environment variables for Next.js. Combines Zod validation with environment variable loading.

  • Downloads: 500K/week
import { createEnv } from '@t3-oss/env-nextjs';
import { z } from 'zod';

export const env = createEnv({
  server: {
    DATABASE_URL: z.string().url(),
    STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
  },
  client: {
    NEXT_PUBLIC_APP_URL: z.string().url(),
  },
  runtimeEnv: {
    DATABASE_URL: process.env.DATABASE_URL,
    STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
    NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
  },
});

// env.DATABASE_URL is guaranteed to be a valid URL string

Unique ID Generation

nanoid

Tiny, secure, URL-friendly unique ID generator. Smaller and faster than UUID.

  • Downloads: 22M/week
  • Size: 0.5KB (gzip)
import { nanoid } from 'nanoid';

nanoid(); // "V1StGXR8_Z5jdHi6B-myT" (21 chars, URL-safe)
nanoid(10); // "IRFa-VaY2b" (custom length)

cuid2

Collision-resistant IDs optimized for horizontal scaling and security. The successor to cuid.

  • Downloads: 800K/week
import { createId } from '@paralleldrive/cuid2';

createId(); // "tz4a98xxat96iws9zmbrgj3a"

HTTP & Data Fetching

ky

Tiny HTTP client built on fetch. Cleaner API than native fetch with retries, JSON shortcuts, and hooks.

  • Downloads: 2M/week
  • Size: 3KB (gzip)
import ky from 'ky';

const data = await ky.get('https://api.example.com/data').json<User[]>();
// Automatic JSON parsing, typed response

ofetch

Universal fetch library from the UnJS ecosystem. Works in Node.js, Deno, Bun, and browsers.

  • Downloads: 8M/week
  • Size: 3KB (gzip)
import { ofetch } from 'ofetch';

const data = await ofetch<User[]>('/api/users', {
  retry: 3,
  retryDelay: 1000,
});

String & Data Manipulation

slugify

Convert strings to URL-friendly slugs.

  • Downloads: 3M/week
  • Size: 1KB
import slugify from 'slugify';
slugify('Hello World!', { lower: true }); // 'hello-world'

superjson

Serialize and deserialize JavaScript values that JSON.stringify can't handle — dates, maps, sets, BigInts, and more.

  • Downloads: 3M/week
  • Size: 4KB
import superjson from 'superjson';

const data = { date: new Date(), set: new Set([1, 2, 3]) };
const json = superjson.stringify(data);
const parsed = superjson.parse(json); // Dates and Sets restored

Our Curated Stack

If you're starting a new TypeScript project, here's our recommended utility stack:

NeedPackageSize
ValidationZod (or Valibot for small bundles)13KB / 0.5KB
Type helperstype-fest + ts-reset0KB (types only)
Pattern matchingts-pattern3KB
Datesdate-fns2-10KB
HTTP clientky or ofetch3KB
IDsnanoid0.5KB
Env validation@t3-oss/env-nextjs

Total runtime impact: ~22KB gzipped. That's a complete utility layer for less than a single React component library.

Common Mistakes with TypeScript Utility Packages

Even experienced TypeScript developers make predictable errors when reaching for these utility packages. Here are the most common ones and how to avoid them.

Zod: Validating at the Wrong Layer

The most common Zod mistake is putting validation only at the API boundary and trusting the result everywhere else. Zod's real power is validating all external data — not just form inputs, but environment variables, localStorage values, third-party API responses, and URL parameters.

// ❌ Common: only validate API inputs
app.post('/users', async (req, res) => {
  const user = UserSchema.parse(req.body); // Good
});

// But then trust data from other sources without validation:
const config = JSON.parse(fs.readFileSync('config.json', 'utf-8')); // ❌ unvalidated

// ✅ Better: validate ALL external data
const ConfigSchema = z.object({
  port: z.number().int().min(1024).max(65535),
  dbUrl: z.string().url(),
});
const config = ConfigSchema.parse(JSON.parse(fs.readFileSync('config.json', 'utf-8')));

ts-reset: Installing It Wrong

ts-reset is a .d.ts file that modifies global TypeScript types. It must be included in your project's type resolution, not just installed. If you install it but don't reference it, nothing changes.

// ❌ ts-reset installed but not referenced
// Nothing changes — TypeScript doesn't know about it

// ✅ Add to your tsconfig.json
{
  "compilerOptions": {
    "types": ["@total-typescript/ts-reset"]
  }
}

// OR: import it once in your project's global types file
// types/global.d.ts
import "@total-typescript/ts-reset";

type-fest: Using It Instead of Built-ins

TypeScript's built-in utility types (Partial, Required, Pick, Omit, ReturnType, Parameters) cover the common cases. Reaching for type-fest for everything leads to unnecessary dependency weight when the standard library suffices.

// ❌ Using type-fest when built-ins work
import type { Except } from 'type-fest';
type UserWithoutPassword = Except<User, 'password'>;

// ✅ Use built-in Omit
type UserWithoutPassword = Omit<User, 'password'>;

// ✅ Reach for type-fest when built-ins fall short
import type { PartialDeep } from 'type-fest';
type PatchPayload = PartialDeep<User>; // Deep partial — no built-in equivalent

nanoid: Using It in Server-Side Rendering Without Seeding

Nanoid uses crypto.getRandomValues in browsers and crypto.randomBytes in Node.js. In SSR contexts, if you generate an ID on the server and render it in HTML, then the client tries to hydrate with a freshly generated (different) ID, you'll get a hydration mismatch. The fix: generate IDs during data fetching (before render) or persist them to state.


Advanced Patterns

Composing Zod Schemas for APIs

Real APIs aren't single schemas — they're composed from reusable pieces. Zod's composition primitives let you build a schema library that mirrors your domain model.

// schemas/base.ts — reusable primitives
const Timestamp = z.object({
  createdAt: z.string().datetime(),
  updatedAt: z.string().datetime(),
});

const PaginatedResponse = <T extends z.ZodTypeAny>(itemSchema: T) =>
  z.object({
    items: z.array(itemSchema),
    total: z.number().int().nonnegative(),
    page: z.number().int().positive(),
    pageSize: z.number().int().positive(),
  });

// schemas/user.ts
const UserBase = z.object({
  id: z.string().cuid2(),
  name: z.string().min(2).max(100),
  email: z.string().email(),
});

export const User = UserBase.merge(Timestamp);
export const UserList = PaginatedResponse(UserBase);
export const CreateUserInput = UserBase.omit({ id: true }).merge(
  z.object({ password: z.string().min(8) })
);

// Type inference flows through
type User = z.infer<typeof User>;
type UserList = z.infer<typeof UserList>;

Using ts-pattern with API Response Discriminated Unions

import { match, P } from 'ts-pattern';

type ApiResponse<T> =
  | { status: 'success'; data: T }
  | { status: 'error'; code: number; message: string }
  | { status: 'loading' }
  | { status: 'idle' };

function renderUserProfile(response: ApiResponse<User>) {
  return match(response)
    .with({ status: 'idle' }, () => <EmptyState />)
    .with({ status: 'loading' }, () => <Skeleton />)
    .with({ status: 'error', code: 401 }, () => <LoginPrompt />)
    .with({ status: 'error', code: P.number }, ({ message }) => <ErrorBanner message={message} />)
    .with({ status: 'success' }, ({ data }) => <UserCard user={data} />)
    .exhaustive(); // TypeScript ensures all cases are handled
}

This pattern — combining discriminated union types with ts-pattern's .exhaustive() — is one of the most powerful TypeScript patterns for handling state machines and API responses. The compiler tells you when you've missed a case.

Type-Safe superjson for tRPC

When using tRPC, superjson as the transformer lets you pass dates, maps, and sets through your API without manual serialization:

// server/trpc.ts
import { initTRPC } from '@trpc/server';
import superjson from 'superjson';

const t = initTRPC.create({
  transformer: superjson, // Dates flow through automatically
});

// Now returning a Date from a procedure works end-to-end
const userRouter = t.router({
  getUser: t.procedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input }) => {
      const user = await db.user.findUnique({ where: { id: input.id } });
      return user; // createdAt (Date) is preserved through superjson
    }),
});

When NOT to Use These Packages

Not every project needs every utility in the list. Here's honest guidance on when to skip them.

Skip Zod when you control both the data producer and consumer (e.g., internal server-to-server calls with TypeScript on both ends). Zod adds runtime overhead and complexity for validation that TypeScript's type system already guarantees at compile time.

Skip ts-reset if you prefer explicit unknown handling and have already audited your codebase for JSON.parse misuse. Some teams find ts-reset's implicit type narrowing surprising when onboarding new developers. Its changes are subtle but global — read the full changelog before adopting.

Skip type-fest if you only need one or two types from it. TypeScript 4.x+ added many utility types that type-fest pioneered. Check if NoInfer, Awaited, or Satisfies (now built-in) solve your problem before adding a dependency.

Skip Day.js if you're building a new greenfield project. The Temporal API is advancing through browser standardization and will eventually land as a native alternative. date-fns is the better bridge choice — modular, tree-shakeable, and its API aligns more closely with the Temporal model.

Skip ofetch in browser-only code where native fetch is available. Adding an abstraction over fetch for simple GET requests adds bytes without meaningful value. Use ky or ofetch when you need retry logic, automatic JSON parsing, or Node.js compatibility.


Ecosystem Integration: These Packages in a Full Stack

Here's how the packages in this list combine in a real Next.js 15 + tRPC application:

┌──────────────────────────────────────────────────────┐
│                   Frontend (Client)                  │
│                                                      │
│  ofetch / ky → tRPC client → Zod-validated response │
│  nanoid for optimistic update IDs                    │
│  ts-pattern for UI state machine rendering           │
│  superjson as tRPC transformer (dates preserved)     │
└──────────────────────────────────────────────────────┘
                        │
                        ▼
┌──────────────────────────────────────────────────────┐
│              tRPC Router (Server)                    │
│                                                      │
│  @t3-oss/env-nextjs validates env at startup         │
│  Zod validates all procedure inputs                  │
│  type-fest utility types on shared interfaces        │
│  ts-reset applied globally (JSON.parse → unknown)    │
│  date-fns for date formatting in responses           │
└──────────────────────────────────────────────────────┘
                        │
                        ▼
┌──────────────────────────────────────────────────────┐
│                    Database Layer                    │
│                                                      │
│  superjson serializes complex types to/from JSON     │
│  nanoid / cuid2 for ID generation                    │
└──────────────────────────────────────────────────────┘

This stack uses every package in this article with purpose. Each solves a specific gap: Zod handles the untrusted boundary, ts-reset hardens the language itself, type-fest handles type manipulation, and superjson handles the serialization boundary.

The packages in this list are selected because they compose well together — each fills a distinct gap without overlapping. Zod handles untrusted boundaries, ts-reset corrects language-level type deficiencies, type-fest provides the deep utility types that TypeScript's built-ins don't cover, ts-pattern adds exhaustive discriminated union handling, and superjson bridges the serialization gap between JavaScript's rich runtime types and JSON's limited wire format. Using all of them together does not create redundancy; it creates a layered type-safety stack where each package's responsibilities are clear. The total runtime footprint — approximately 22KB gzipped — is smaller than a single icon library, making this a practical addition even to bundle-sensitive applications.

Browse all these packages on PkgPulse to compare health scores, download trends, and alternatives.

See also: Yup vs Zod and Superstruct vs Zod, AJV vs Zod vs Valibot: Speed, Bundle Size & TypeScript (2026).

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.