Skip to main content

The New Wave of TypeScript-First Libraries in 2026

·PkgPulse Team

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"
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.


Compare TypeScript-first library health on PkgPulse.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.