The New Wave of TypeScript-First Libraries in 2026
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:
// 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);
});
The TypeScript-First Library Checklist
What makes a library "TypeScript-first" vs "has TypeScript types":
| Criterion | "Has Types" | "TypeScript-First" |
|---|---|---|
| Source | DefinitelyTyped or bolt-on | Written in TypeScript from start |
| Type source | Separate .d.ts files | Inferred from implementation |
| Runtime/type sync | Can drift | Guaranteed in sync |
| Inference | Manual type annotations needed | Types inferred automatically |
| Examples | @types/express, @types/node | Zod, tRPC, Drizzle |
| API design | Designed for JS, typed later | Designed 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.
Compare TypeScript-first library health on PkgPulse.
See the live comparison
View prisma vs. drizzle on PkgPulse →