Is inferring types from runtime schema an anti-pattern in type-driven design?
This is a problem very specific TypeScript programming language. I do not think other (statically + strongly) typed languages have this problem and only support type-first development. When I am using a library like zod, I have two options to define my types. // Type-first (Types are the source of truth) import { z } from "zod"; type User = { id: number; name: string; email?: string; }; const userSchema: z.ZodType = z.object({ id: z.number(), name: z.string(), email: z.string().optional(), }); And, I can also infer types from my schema: // Schema-first (Schema is the source of truth) const userSchema = z.object({ id: z.number(), name: z.string(), email: z.string().optional(), }); type User = z.infer; // The `typeof` simply doesn't not exist in other programming languages. Conceptually speaking, the actual value `userSchema` is simply being lifted from runtime-plane into a compile time type-plane. The second one is extremely compact and is getting more and more popular due to various libraries like trpc, hono/rpc, etc. However, I feel it is anti-pattern as it mixes two separate concerns. The type-driven design is about thinking about your system in types and defining in types as much as possible. There are many categories but mainly that includes the I/O types (API boundary types), domain types (Business domain types), persistence types (Database types) and infra types. I am building a considerably-large scale application with many contributors and thus consider system to be well-defined contract in terms of types so that it is easy to reason about system. The schema should always follow the types and not the other way. Is this a valid concern from architectural perspective or do I need adjustment in thinking about system design as a whole?

This is a problem very specific TypeScript programming language. I do not think other (statically + strongly) typed languages have this problem and only support type-first development. When I am using a library like zod
, I have two options to define my types.
// Type-first (Types are the source of truth)
import { z } from "zod";
type User = {
id: number;
name: string;
email?: string;
};
const userSchema: z.ZodType = z.object({
id: z.number(),
name: z.string(),
email: z.string().optional(),
});
And, I can also infer types from my schema:
// Schema-first (Schema is the source of truth)
const userSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().optional(),
});
type User = z.infer;
// The `typeof` simply doesn't not exist in other programming languages. Conceptually speaking, the actual value `userSchema` is simply being lifted from runtime-plane into a compile time type-plane.
The second one is extremely compact and is getting more and more popular due to various libraries like trpc
, hono/rpc
, etc. However, I feel it is anti-pattern as it mixes two separate concerns.
The type-driven design is about thinking about your system in types and defining in types as much as possible. There are many categories but mainly that includes the I/O types (API boundary types), domain types (Business domain types), persistence types (Database types) and infra types.
I am building a considerably-large scale application with many contributors and thus consider system to be well-defined contract in terms of types so that it is easy to reason about system. The schema should always follow the types and not the other way.
Is this a valid concern from architectural perspective or do I need adjustment in thinking about system design as a whole?