Skip to main content

The New Wave of TypeScript-First Libraries in 2026

·PkgPulse Team
0

TL;DR

TypeScript-first means the types ARE the API. The wave of TypeScript-first libraries that emerged 2021-2024 shares a design philosophy: the TypeScript interface is not an afterthought added via DefinitelyTyped — it's the core product. Zod infers TypeScript types from runtime schemas. tRPC uses TypeScript inference for end-to-end type safety without code generation. Drizzle infers database query result types from your schema. This isn't just "has types" — it's "types are the source of truth."

Key Takeaways

  • Zod: ~12M weekly downloads — runtime schema = TypeScript types, zero generation step
  • tRPC: ~2M downloads — full-stack TypeScript without code gen or schema files
  • Drizzle ORM: ~2M downloads — SQL in TypeScript, types inferred from schema definition
  • TypeBox: ~3M downloads — JSON Schema + TypeScript types from the same definition
  • The pattern — define once, get runtime validation AND TypeScript types

The Old Way: Types as Documentation

Before TypeScript-first libraries, types were documentation added on top of a JavaScript API. The DefinitelyTyped model, which peaked in the years when TypeScript was gaining adoption but before library authors had fully committed to it, required a fundamentally different workflow than what developers use today.

The core problem with DefinitelyTyped was the separation between the library and its types. The library was written in JavaScript with its own release cadence. The types lived in a separate repository, maintained by volunteers who might not be the library authors, and published under @types/ scope. When the library released a new version, the types might lag by days, weeks, or months. When the library introduced a breaking change, the types might be wrong for the current version. And critically, the types described the API's shape but had no connection to what the code actually did at runtime.

This created a subtle but pervasive problem: TypeScript gave you type safety at compile time for the shape of your function arguments and return values, but runtime behavior was untyped. You could pass an id parameter typed as string to a route handler, and TypeScript would tell you everything was fine — but at runtime, if the actual HTTP request contained something unexpected, there was no validation layer to catch it. The types were documentation that the compiler enforced, not constraints that the runtime enforced.

The shift happened because of TypeScript's improving capability for complex type inference. Once TypeScript could infer deeply nested types from generic constraints, the possibility of building libraries where the types themselves were derived from the runtime schema became achievable. Zod was among the first to make this work elegantly, and it demonstrated to the ecosystem what TypeScript-first library design could feel like.

// DefinitelyTyped pattern (types as documentation)
// The library is written in JavaScript
// @types/express provides TypeScript declarations separately

import express from 'express';  // JavaScript library
import { Request, Response } from 'express';  // Types from @types/express

// Problem: types can drift from implementation
// Problem: types don't validate at runtime
// Problem: runtime value doesn't know about TypeScript types

app.get('/user/:id', async (req: Request, res: Response) => {
  const id = req.params.id;  // TypeScript: string
  // At runtime: could be anything — no validation happened
  const user = await User.findById(id);  // If id is "undefined", TypeScript doesn't catch it
  res.json(user);
});

The New Way: Types as the API

Zod: Runtime + Types From One Definition

// Zod — define schema once, get types AND validation
import { z } from 'zod';

// Define the schema
const CreateUserSchema = z.object({
  name: z.string().min(1, 'Name required').max(100),
  email: z.string().email('Invalid email'),
  age: z.number().int().min(18).max(120).optional(),
  role: z.enum(['user', 'admin', 'moderator']).default('user'),
  preferences: z.object({
    newsletter: z.boolean().default(false),
    theme: z.enum(['light', 'dark', 'system']).default('system'),
  }).optional(),
});

// Type is inferred automatically — no separate interface!
type CreateUser = z.infer<typeof CreateUserSchema>;
// {
//   name: string;
//   email: string;
//   age?: number | undefined;
//   role: 'user' | 'admin' | 'moderator';
//   preferences?: { newsletter: boolean; theme: 'light' | 'dark' | 'system' } | undefined;
// }

// Runtime validation
const result = CreateUserSchema.safeParse(req.body);
if (!result.success) {
  // result.error.flatten().fieldErrors — organized by field
  return res.status(400).json(result.error.flatten());
}
const user = result.data;  // TypeScript knows: user is CreateUser
// Zod — transforms (parse + transform in one step)
const DateStringSchema = z.string()
  .datetime()
  .transform(str => new Date(str));

type DateValue = z.infer<typeof DateStringSchema>;  // Date (not string!)

// Zod — refine (custom validation)
const PasswordSchema = z.string()
  .min(8)
  .refine(val => /[A-Z]/.test(val), 'Must contain uppercase')
  .refine(val => /[0-9]/.test(val), 'Must contain number');

tRPC: End-to-End Types Without Code Gen

// tRPC — TypeScript-first RPC
// Server: define procedures with TypeScript
// Client: get full type inference with zero code generation

// server/router.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';

const t = initTRPC.create();

export const appRouter = t.router({
  packages: t.router({
    get: t.procedure
      .input(z.object({ name: z.string() }))
      .query(async ({ input }) => {
        return await fetchPackage(input.name);
        // Return type is automatically inferred by TypeScript
      }),

    search: t.procedure
      .input(z.object({
        query: z.string(),
        page: z.number().default(1),
        limit: z.number().max(50).default(20),
      }))
      .query(async ({ input }) => {
        return await searchPackages(input);
      }),

    compare: t.procedure
      .input(z.object({
        packageA: z.string(),
        packageB: z.string(),
      }))
      .mutation(async ({ input }) => {
        return await createComparison(input.packageA, input.packageB);
      }),
  }),
});

export type AppRouter = typeof appRouter;

// client/api.ts — no code generation needed!
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/router';

export const trpc = createTRPCReact<AppRouter>();

// component.tsx — fully typed
function PackageSearch() {
  const searchQuery = trpc.packages.search.useQuery({
    query: 'react',
    page: 1,
  });

  // searchQuery.data is automatically typed as the return type of the server query
  // No manual type annotation needed
  // No schema file needed
  // No code generation step needed

  return <div>{searchQuery.data?.results.map(p => p.name)}</div>;
}

Drizzle: Database Schema → TypeScript Types

// Drizzle ORM — define schema, types are inferred
import { pgTable, serial, varchar, integer, boolean } from 'drizzle-orm/pg-core';
import { InferSelectModel, InferInsertModel } from 'drizzle-orm';

export const packages = pgTable('packages', {
  id: serial('id').primaryKey(),
  name: varchar('name', { length: 255 }).notNull().unique(),
  description: varchar('description', { length: 1000 }),
  weeklyDownloads: integer('weekly_downloads').notNull().default(0),
  healthScore: integer('health_score').notNull().default(0),
  isDeprecated: boolean('is_deprecated').notNull().default(false),
});

// Types inferred from schema definition — no separate interface!
type Package = InferSelectModel<typeof packages>;
// {
//   id: number;
//   name: string;
//   description: string | null;
//   weeklyDownloads: number;
//   healthScore: number;
//   isDeprecated: boolean;
// }

type NewPackage = InferInsertModel<typeof packages>;
// { name: string; description?: string | null; weeklyDownloads?: number; ... }

// Query result types are also inferred
const result = await db
  .select({ name: packages.name, score: packages.healthScore })
  .from(packages)
  .where(eq(packages.isDeprecated, false));

// result is: { name: string; score: number }[]  — TypeScript knows!

TypeBox: JSON Schema + TypeScript Types

// TypeBox — JSON Schema for validation + TypeScript types from one source
import { Type, Static } from '@sinclair/typebox';
import { Value } from '@sinclair/typebox/value';

const UserSchema = Type.Object({
  name: Type.String({ minLength: 1 }),
  email: Type.String({ format: 'email' }),
  age: Type.Optional(Type.Integer({ minimum: 18 })),
});

type User = Static<typeof UserSchema>;
// Same result as Zod.infer — TypeScript infers from the schema

// Validate
const valid = Value.Check(UserSchema, req.body);  // boolean
const parsed = Value.Parse(UserSchema, req.body); // throws on invalid

// FastAPI integration — TypeBox schemas work natively with Fastify
fastify.post('/users', {
  schema: { body: UserSchema },  // JSON Schema for Fastify
}, async (request) => {
  const user = request.body;  // Typed as User!
  return createUser(user);
});

Why TypeScript-First Changed Library Design Fundamentally

The philosophical shift from "JavaScript library + TypeScript types" to "TypeScript as the design medium" is not just a technical preference. It fundamentally changes what APIs are possible to build.

When you design a library for JavaScript first, your API surface is constrained by what JavaScript can express cleanly. You think in terms of objects, functions, and callbacks. TypeScript types are then layered on top, typically as explicit type parameters or manually maintained declaration files. The API design decisions were made before TypeScript constraints were considered.

When you design a library for TypeScript first, you gain access to an entirely different design space. TypeScript's advanced type system — generic constraints, conditional types, template literal types, mapped types, infer keyword — lets you build APIs where the types themselves do meaningful work. The API can make guarantees at compile time that would be impossible or awkward in plain JavaScript.

Zod's discriminated union inference demonstrates this clearly. When you define a Zod discriminated union schema — say, different event types where a type field determines the shape of the rest of the object — TypeScript narrows the type in the arms of your if-else or switch statement. This isn't just documentation; the TypeScript compiler enforces it. You couldn't replicate this API design in a JavaScript-first library because the safety guarantee requires the type system to do work at compile time.

tRPC's procedure chaining is another example of an API design that's only possible because of TypeScript. The type of the client's method call is derived from the server's router definition through TypeScript inference. This chain of type derivation — server router type → exported AppRouter type → client generic parameter → individual procedure call return types — depends on TypeScript's ability to trace type relationships across module boundaries. A JavaScript-first RPC library would require code generation (like gRPC's protobuf compilers or GraphQL's codegen) to achieve the same safety. tRPC replaces code generation with TypeScript inference, which means no build step, no generated files to maintain, no sync issues between generated types and actual implementation.

Drizzle's select/join type inference works similarly. When you call db.select({ name: packages.name, score: packages.healthScore }), Drizzle's TypeScript implementation tracks the selected columns and infers the exact shape of the return type. Add a join and the joined columns appear in the return type. Exclude a column from the select and it disappears from the type. This API would require either code generation or explicit type annotations to achieve in a JavaScript-first world. TypeScript-first design makes it possible with pure inference.

The developer experience implication is significant. TypeScript-first APIs catch an entire class of bugs — wrong field names, missing required fields, incorrect value types — at compile time rather than at runtime. For teams that value fast feedback loops in development, this is a meaningful productivity gain. The time between writing incorrect code and getting feedback drops from "when you hit that code path in testing" to "immediately, as you type."


The Performance Cost (And Why It Doesn't Matter)

TypeScript-first libraries have a reputation for being slower at runtime than hand-optimized JavaScript equivalents. The reputation is partially deserved but often overstated, and the cases where it matters are narrower than you might expect.

Zod has been the primary target of runtime performance criticism, and the criticism has some merit. Zod v3's validation performance is measurably slower than faster alternatives. For simple schema validation, Zod processes roughly 500K to 1M validations per second on typical hardware. Valibot, which was designed explicitly to be faster and more tree-shakeable, processes 2-3M validations per second for equivalent schemas. ArkType, which compiles TypeScript-native syntax to optimized validators, claims 100x improvement over Zod in some benchmarks — more accurately, for simple types, ArkType validates extremely fast because it compiles schemas at definition time rather than evaluating them at parse time.

Why this usually doesn't matter: request validation is rarely the bottleneck in a web application. A typical API request that parses a request body, queries a database, and returns a response spends most of its time on the database query. If the database query takes 20ms and the Zod validation takes 0.1ms, optimizing validation gives you a 0.5% improvement. The bottleneck analysis consistently shows that the time saved by switching from Zod to a faster validator doesn't show up in production p50 or p99 latencies for typical CRUD applications.

When runtime validation performance does matter: high-throughput event processing pipelines, message queue consumers that process thousands of events per second, parsers that are called millions of times in tight loops, or any context where you're doing validation in a hot path without I/O. A financial data pipeline processing 100K events per second with Zod validation at each step might consume meaningful CPU on validation. In those contexts, ArkType or a purpose-built validator is the right choice.

The Valibot comparison deserves specific attention because Valibot is the most direct Zod alternative with explicit performance and bundle-size goals. Valibot is modular and tree-shakeable — you import only the validators you use, which results in smaller bundles for front-end code. Zod v4 (released in 2025) addressed both the bundle size and the raw performance gaps substantially. Zod v4 is roughly 3x faster than Zod v3 and significantly smaller, which made the Valibot performance argument less compelling for most use cases.

The practical choice: use Zod unless you have measured evidence that validation is a bottleneck, in which case evaluate Valibot or ArkType based on your specific validation patterns and bundle size requirements.


Beyond Zod: The Validation Library Landscape

The TypeScript-first validation ecosystem has diversified considerably since Zod established the pattern. Understanding the current landscape helps you pick the right tool for the specific constraints of your project.

Zod v4 is the default choice for most projects. It benefits from the network effects of being the first TypeScript-first validation library to gain widespread adoption. Zod integrations exist for React Hook Form, tRPC, Fastify, Hono, Elysia, and virtually every other TypeScript framework you might want to use. The ecosystem around Zod — including zod-to-json-schema, zod-openapi, and hundreds of community packages — is broader than any alternative. Zod v4's performance improvements closed the gap with competitors. The main remaining weakness is bundle size for front-end applications where you're only using a subset of Zod's features, since tree-shaking support is still less aggressive than Valibot.

Valibot takes a modular approach where every validator is a separate import. This makes Valibot exceptionally tree-shakeable — a form that only uses string and number validators pulls in only string and number validators from the bundle, not the entire library. This matters significantly for front-end bundle sizes. Valibot's API is slightly more verbose than Zod's, but it's a deliberate tradeoff for the bundle efficiency gains. For applications where front-end bundle size is a primary constraint, Valibot is worth serious consideration.

ArkType uses TypeScript's own syntax to define schemas. Instead of z.string().email(), you write "string.email" as a string literal, and ArkType parses it using TypeScript's type system. The result is schemas that look almost like TypeScript type annotations and that compile to extremely fast validators. The learning curve is higher — ArkType's syntax is unusual — and the ecosystem integration is narrower than Zod's. For teams that want maximum performance and are willing to invest in learning a different API style, ArkType is the technically superior choice.

TypeBox occupies a specific niche: it produces JSON Schema output as a first-class output, not as an afterthought. This makes TypeBox the natural choice when you need both TypeScript types and a JSON Schema document — for example, when building a public API that needs an OpenAPI specification generated from the same source that your TypeScript types come from. TypeBox integrates natively with Fastify's JSON Schema validation, which means Fastify can use AJV (its compiled JSON Schema validator) for the actual validation rather than TypeBox's own runtime, giving you excellent performance with TypeScript types.

How to pick: Zod for general use. Valibot when front-end bundle size is a constraint. ArkType when you're in a high-throughput pipeline and have measured that validation is the bottleneck. TypeBox when you need JSON Schema output alongside TypeScript types.


The T3 Stack and Its Successors

The create-t3-app project, launched by Theo Browne in 2022, deserves significant credit for demonstrating how TypeScript-first libraries cluster into coherent, productive stacks. T3 showed that Zod, tRPC, and Prisma (and later Drizzle) weren't just individual library choices — they were components of a philosophy about how to build full-stack TypeScript applications.

The T3 stack at its core is Next.js as the full-stack framework, tRPC for the API layer between front-end and back-end, Prisma or Drizzle for database access, Tailwind for styling, Auth.js for authentication, and t3-env for environment variable validation. Every layer uses TypeScript inference rather than code generation or separate type annotation files. An environment variable is defined once in the t3-env schema, and TypeScript knows its type everywhere in the application. A database table is defined once in the Drizzle schema, and TypeScript knows the query result types everywhere. An API route is defined once in the tRPC router, and TypeScript provides autocompletion on the client.

The T3 stack became popular quickly because it solved a real problem: full-stack TypeScript applications had many potential type-safety holes (env vars, API contracts, database types) that required either manual synchronization or code generation to close. T3 closed all those holes with inference.

Several successor stacks have emerged as the TypeScript-first ecosystem matured. The Hono + Drizzle + Zod stack has become popular for non-Next.js applications, particularly API servers and backends that don't need SSR. Hono is a small, fast, runtime-agnostic HTTP framework that uses TypeScript throughout — its HonoRequest type system and validator middleware (including a Zod integration) provide end-to-end typing similar to tRPC but for traditional HTTP APIs rather than RPC. Hono runs on Node.js, Bun, Deno, and Cloudflare Workers, making it a good fit for teams who want runtime flexibility alongside TypeScript safety.

The Astro + tRPC combination has become popular for content-heavy sites that need some dynamic API functionality. Astro's islands architecture works well with tRPC for the dynamic portions of the page, while the static parts benefit from Astro's build-time optimization.

The common thread in all these successor stacks is the same TypeScript-first philosophy: define data shapes once, infer types everywhere, eliminate code generation steps. The specific libraries change based on the deployment target and application type, but the design principle remains constant. TypeScript-first tools cluster together naturally because they share the same foundational assumption — that the TypeScript compiler should do as much work as possible, and that manual type annotations should be the exception rather than the rule.

For a detailed comparison of the two most popular ORM choices in these stacks, see Prisma vs Drizzle.


What TypeScript-First Looks Like for Library Authors

If you're building a library and want to achieve TypeScript-first design, the techniques involved are more advanced than simply "write it in TypeScript." Here are the key patterns that separate TypeScript-first libraries from libraries that merely ship TypeScript types.

The satisfies operator, added in TypeScript 4.9, is one of the most useful tools for library design. It lets you validate that an object satisfies a type without widening the inferred type. This lets library authors write helper functions where the return type is exactly what the user constructed — not widened to a general interface — which enables inference to flow through the library's API boundaries.

Template literal types enable APIs where string arguments have structured validation at compile time. A library that accepts route paths can use template literal types to ensure the path starts with /. A library that accepts SQL-like query strings can use template literal types to enforce basic syntax. These are purely compile-time constraints with no runtime cost.

Conditional types and the infer keyword are the engines of most TypeScript-first library type inference. When Drizzle infers query result types, it uses conditional types to extract the column types from the schema object. When tRPC infers client procedure return types, it uses conditional types to unwrap the Promise from the server handler's return type. Learning to write conditional types fluently is the key skill for TypeScript-first library design.

Testing TypeScript types is often overlooked but important. Libraries like tsd and expect-type let you write test assertions about TypeScript types, similar to how you'd write assertions about runtime values. expect-type in particular has a clean API: expectTypeOf(myFunction(input)).toEqualTypeOf<ExpectedReturnType>(). Including type tests in your test suite catches regressions where a refactoring inadvertently changes the public type API in a breaking way.

Shipping TypeScript source rather than only compiled .d.ts declaration files improves the experience for users in several ways. IDEs can jump to the actual source code for Go To Definition rather than landing in a generated declaration file. Type errors include the actual implementation context rather than abstract interface descriptions. For smaller libraries, shipping the source directly is increasingly common.

The ecosystem of TypeScript-first tooling is visible in the packages driving adoption growth across the npm ecosystem. See the MCP libraries and Node.js tooling ecosystem for how TypeScript-first patterns are extending into AI tooling and protocol implementations.


The TypeScript-First Library Checklist

What makes a library "TypeScript-first" vs "has TypeScript types":

Criterion"Has Types""TypeScript-First"
SourceDefinitelyTyped or bolt-onWritten in TypeScript from start
Type sourceSeparate .d.ts filesInferred from implementation
Runtime/type syncCan driftGuaranteed in sync
InferenceManual type annotations neededTypes inferred automatically
Examples@types/express, @types/nodeZod, tRPC, Drizzle
API designDesigned for JS, typed laterDesigned for TypeScript

The New Default Stack

The TypeScript-first libraries cluster into a coherent "new default stack" for 2026:

Data Validation:     Zod
API Layer:           tRPC (full-stack TypeScript)
Database ORM:        Drizzle (TypeScript schema → types)
HTTP Client:         ky (ESM, tiny, typed responses via generics)
Env Validation:      t3-env (Zod-based env schema)
Form Validation:     React Hook Form + Zod resolver
Auth:                Auth.js v5 (TypeScript adapter types)

This stack is sometimes called the "T3 stack" (popularized by create-t3-app). Every layer from env vars to database queries to API calls to form validation uses TypeScript inference — no manual type annotations, no code generation, no schema files separate from the implementation.

For developers evaluating Zod as the validation layer in this stack, the Zod vs Yup comparison covers the technical and ergonomic differences in depth.


Compare TypeScript-first library health on PkgPulse.

See also: Yup vs Zod and Superstruct vs Zod, Zod vs Yup: TypeScript Validation 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.