Tools
Tools: Implementing the Newtype Pattern with Zod: Enhancing Type Safety in TypeScript
2026-01-29
0 views
admin
The Problem of Structural Typing in TypeScript ## Solution with Zod's Branded Types ## Internal Structure of Types ## Practical Example 1: Distinguishing Email Addresses and Usernames ## Practical Example 2: Distinguishing Currencies ## Practical Example 3: Validating API Responses ## Notes on Branded Types ## 1. No Impact on Runtime ## 2. Parsing is Mandatory ## 3. Control Over Input and Output Directions ## Comparison with Pydantic ## Conclusion ## Reference Links Originally published on 2026-01-22
Original article (Japanese): Zodで実装するNewtype Pattern: TypeScriptに欠けている型安全性を補う The type system of TypeScript is based on structural typing. This means that different types are treated as the same if their structures are identical. As a result, bugs can occur when values that should be distinguished are mistakenly confused. Using branded types from Zod can solve this problem. This is a practical approach to implementing the Newtype Pattern in TypeScript, which assigns "meaningfully different types" to the same primitive type to prevent mix-ups. In this article, we will introduce the basics of branded types and their practical usage with examples. In TypeScript, the following code passes without errors: Since both UserId and PostId are of type string, TypeScript cannot distinguish between them. This could lead to fetching the wrong records from the database at runtime. By using Zod's .brand<> method, we can "stamp" the types to distinguish them. Branded types internally look like this: A special type z.$brand<"UserId"> is added as an intersection type (&), allowing the same string to be treated as different types. When validating forms, you may want to distinguish between email addresses and usernames: When dealing with amounts, mixing up currencies can lead to serious issues. An example of validating data obtained from an external API and treating it as a branded type: Branded types function only for static type checking. At runtime, they are treated as regular values. To obtain a branded type, you must execute .parse() or .safeParse() with a Zod schema. Starting from Zod 4.2, you can specify the direction of the brand. Python also has similar functionality in Pydantic. Both share the philosophy of "inferring types from schema definitions," but Zod's branded types are specifically focused on type-level distinctions. By using Zod's branded types, you can address the weaknesses of TypeScript's structural typing and gain the following benefits: Especially in large projects or applications that frequently interact with external APIs, considering the introduction of branded types is worthwhile. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK:
type UserId = string;
type PostId = string; function getUser(userId: UserId) { // Fetch user
} function getPost(postId: PostId) { // Fetch post
} const userId: UserId = "user_123";
const postId: PostId = "post_456"; getUser(postId); // ❌ Ideally, this should throw an error, but it passes
getPost(userId); // ❌ This also passes Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
type UserId = string;
type PostId = string; function getUser(userId: UserId) { // Fetch user
} function getPost(postId: PostId) { // Fetch post
} const userId: UserId = "user_123";
const postId: PostId = "post_456"; getUser(postId); // ❌ Ideally, this should throw an error, but it passes
getPost(userId); // ❌ This also passes CODE_BLOCK:
type UserId = string;
type PostId = string; function getUser(userId: UserId) { // Fetch user
} function getPost(postId: PostId) { // Fetch post
} const userId: UserId = "user_123";
const postId: PostId = "post_456"; getUser(postId); // ❌ Ideally, this should throw an error, but it passes
getPost(userId); // ❌ This also passes CODE_BLOCK:
import { z } from "zod"; const UserIdSchema = z.string().brand<"UserId">();
const PostIdSchema = z.string().brand<"PostId">(); type UserId = z.infer<typeof UserIdSchema>;
type PostId = z.infer<typeof PostIdSchema>; function getUser(userId: UserId) { // Fetch user
} function getPost(postId: PostId) { // Fetch post
} const userId = UserIdSchema.parse("user_123");
const postId = PostIdSchema.parse("post_456"); // @ts-expect-error PostId is not assignable to UserId
getUser(postId); // @ts-expect-error UserId is not assignable to PostId
getPost(userId); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
import { z } from "zod"; const UserIdSchema = z.string().brand<"UserId">();
const PostIdSchema = z.string().brand<"PostId">(); type UserId = z.infer<typeof UserIdSchema>;
type PostId = z.infer<typeof PostIdSchema>; function getUser(userId: UserId) { // Fetch user
} function getPost(postId: PostId) { // Fetch post
} const userId = UserIdSchema.parse("user_123");
const postId = PostIdSchema.parse("post_456"); // @ts-expect-error PostId is not assignable to UserId
getUser(postId); // @ts-expect-error UserId is not assignable to PostId
getPost(userId); CODE_BLOCK:
import { z } from "zod"; const UserIdSchema = z.string().brand<"UserId">();
const PostIdSchema = z.string().brand<"PostId">(); type UserId = z.infer<typeof UserIdSchema>;
type PostId = z.infer<typeof PostIdSchema>; function getUser(userId: UserId) { // Fetch user
} function getPost(postId: PostId) { // Fetch post
} const userId = UserIdSchema.parse("user_123");
const postId = PostIdSchema.parse("post_456"); // @ts-expect-error PostId is not assignable to UserId
getUser(postId); // @ts-expect-error UserId is not assignable to PostId
getPost(userId); CODE_BLOCK:
type UserId = string & z.$brand<"UserId">;
type PostId = string & z.$brand<"PostId">; Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
type UserId = string & z.$brand<"UserId">;
type PostId = string & z.$brand<"PostId">; CODE_BLOCK:
type UserId = string & z.$brand<"UserId">;
type PostId = string & z.$brand<"PostId">; CODE_BLOCK:
const EmailSchema = z.email().brand<"Email">();
const UsernameSchema = z.string().min(3).max(20).brand<"Username">(); type Email = z.infer<typeof EmailSchema>;
type Username = z.infer<typeof UsernameSchema>; function sendEmail(to: Email, subject: string) { // Email sending logic
} function createUser(username: Username) { // User creation logic
} const email = EmailSchema.parse("[email protected]");
const username = UsernameSchema.parse("john_doe"); sendEmail(email, "Welcome!"); // ✅ OK
// @ts-expect-error Username is not assignable to Email
sendEmail(username, "Welcome!"); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
const EmailSchema = z.email().brand<"Email">();
const UsernameSchema = z.string().min(3).max(20).brand<"Username">(); type Email = z.infer<typeof EmailSchema>;
type Username = z.infer<typeof UsernameSchema>; function sendEmail(to: Email, subject: string) { // Email sending logic
} function createUser(username: Username) { // User creation logic
} const email = EmailSchema.parse("[email protected]");
const username = UsernameSchema.parse("john_doe"); sendEmail(email, "Welcome!"); // ✅ OK
// @ts-expect-error Username is not assignable to Email
sendEmail(username, "Welcome!"); CODE_BLOCK:
const EmailSchema = z.email().brand<"Email">();
const UsernameSchema = z.string().min(3).max(20).brand<"Username">(); type Email = z.infer<typeof EmailSchema>;
type Username = z.infer<typeof UsernameSchema>; function sendEmail(to: Email, subject: string) { // Email sending logic
} function createUser(username: Username) { // User creation logic
} const email = EmailSchema.parse("[email protected]");
const username = UsernameSchema.parse("john_doe"); sendEmail(email, "Welcome!"); // ✅ OK
// @ts-expect-error Username is not assignable to Email
sendEmail(username, "Welcome!"); CODE_BLOCK:
const USDSchema = z.number().positive().brand<"USD">();
const JPYSchema = z.number().int().positive().brand<"JPY">(); type USD = z.infer<typeof USDSchema>;
type JPY = z.infer<typeof JPYSchema>; function chargeUSD(amount: USD) { console.log(`Charging $${amount}`);
} function chargeJPY(amount: JPY) { console.log(`Charging ¥${amount}`);
} const usd = USDSchema.parse(100.50);
const jpy = JPYSchema.parse(10000); chargeUSD(usd); // ✅ OK
// @ts-expect-error JPY is not assignable to USD
chargeUSD(jpy); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
const USDSchema = z.number().positive().brand<"USD">();
const JPYSchema = z.number().int().positive().brand<"JPY">(); type USD = z.infer<typeof USDSchema>;
type JPY = z.infer<typeof JPYSchema>; function chargeUSD(amount: USD) { console.log(`Charging $${amount}`);
} function chargeJPY(amount: JPY) { console.log(`Charging ¥${amount}`);
} const usd = USDSchema.parse(100.50);
const jpy = JPYSchema.parse(10000); chargeUSD(usd); // ✅ OK
// @ts-expect-error JPY is not assignable to USD
chargeUSD(jpy); CODE_BLOCK:
const USDSchema = z.number().positive().brand<"USD">();
const JPYSchema = z.number().int().positive().brand<"JPY">(); type USD = z.infer<typeof USDSchema>;
type JPY = z.infer<typeof JPYSchema>; function chargeUSD(amount: USD) { console.log(`Charging $${amount}`);
} function chargeJPY(amount: JPY) { console.log(`Charging ¥${amount}`);
} const usd = USDSchema.parse(100.50);
const jpy = JPYSchema.parse(10000); chargeUSD(usd); // ✅ OK
// @ts-expect-error JPY is not assignable to USD
chargeUSD(jpy); COMMAND_BLOCK:
const UserResponseSchema = z.object({ id: z.string().brand<"UserId">(), email: z.email().brand<"Email">(), createdAt: z.iso.datetime({ offset: true }).brand<"ISODateTime">(),
}); type UserResponse = z.infer<typeof UserResponseSchema>; async function fetchUser(userId: string): Promise<UserResponse> { const response = await fetch(`/api/users/${userId}`); const data = await response.json(); // Runtime validation + branding return UserResponseSchema.parse(data);
} function displayUser(user: UserResponse) { console.log(`User ID: ${user.id}`); console.log(`Email: ${user.email}`);
} const user = await fetchUser("user_123");
displayUser(user); // ✅ Type safe Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
const UserResponseSchema = z.object({ id: z.string().brand<"UserId">(), email: z.email().brand<"Email">(), createdAt: z.iso.datetime({ offset: true }).brand<"ISODateTime">(),
}); type UserResponse = z.infer<typeof UserResponseSchema>; async function fetchUser(userId: string): Promise<UserResponse> { const response = await fetch(`/api/users/${userId}`); const data = await response.json(); // Runtime validation + branding return UserResponseSchema.parse(data);
} function displayUser(user: UserResponse) { console.log(`User ID: ${user.id}`); console.log(`Email: ${user.email}`);
} const user = await fetchUser("user_123");
displayUser(user); // ✅ Type safe COMMAND_BLOCK:
const UserResponseSchema = z.object({ id: z.string().brand<"UserId">(), email: z.email().brand<"Email">(), createdAt: z.iso.datetime({ offset: true }).brand<"ISODateTime">(),
}); type UserResponse = z.infer<typeof UserResponseSchema>; async function fetchUser(userId: string): Promise<UserResponse> { const response = await fetch(`/api/users/${userId}`); const data = await response.json(); // Runtime validation + branding return UserResponseSchema.parse(data);
} function displayUser(user: UserResponse) { console.log(`User ID: ${user.id}`); console.log(`Email: ${user.email}`);
} const user = await fetchUser("user_123");
displayUser(user); // ✅ Type safe COMMAND_BLOCK:
const userId = UserIdSchema.parse("user_123");
console.log(typeof userId); // => "string" Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
const userId = UserIdSchema.parse("user_123");
console.log(typeof userId); // => "string" COMMAND_BLOCK:
const userId = UserIdSchema.parse("user_123");
console.log(typeof userId); // => "string" CODE_BLOCK:
const userId: UserId = "user_123"; // ❌ Error: string cannot be assigned to UserId
const userId = UserIdSchema.parse("user_123"); // ✅ OK Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
const userId: UserId = "user_123"; // ❌ Error: string cannot be assigned to UserId
const userId = UserIdSchema.parse("user_123"); // ✅ OK CODE_BLOCK:
const userId: UserId = "user_123"; // ❌ Error: string cannot be assigned to UserId
const userId = UserIdSchema.parse("user_123"); // ✅ OK CODE_BLOCK:
// Default: Brand is applied only to output
z.string().brand<"UserId">();
z.string().brand<"UserId", "in">(); // Same (Zod 4.2+) // Brand is applied only to input
z.string().brand<"UserId", "out">(); // Brand is applied to both input and output
z.string().brand<"UserId", "inout">(); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
// Default: Brand is applied only to output
z.string().brand<"UserId">();
z.string().brand<"UserId", "in">(); // Same (Zod 4.2+) // Brand is applied only to input
z.string().brand<"UserId", "out">(); // Brand is applied to both input and output
z.string().brand<"UserId", "inout">(); CODE_BLOCK:
// Default: Brand is applied only to output
z.string().brand<"UserId">();
z.string().brand<"UserId", "in">(); // Same (Zod 4.2+) // Brand is applied only to input
z.string().brand<"UserId", "out">(); // Brand is applied to both input and output
z.string().brand<"UserId", "inout">(); CODE_BLOCK:
from pydantic import BaseModel, Field
from typing import Annotated UserId = Annotated[str, Field(pattern=r'^user_\d+$')] class User(BaseModel): id: UserId Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
from pydantic import BaseModel, Field
from typing import Annotated UserId = Annotated[str, Field(pattern=r'^user_\d+$')] class User(BaseModel): id: UserId CODE_BLOCK:
from pydantic import BaseModel, Field
from typing import Annotated UserId = Annotated[str, Field(pattern=r'^user_\d+$')] class User(BaseModel): id: UserId CODE_BLOCK:
const UserIdSchema = z.string().regex(/^user_\d+$/).brand<"UserId">();
type UserId = z.infer<typeof UserIdSchema>; Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
const UserIdSchema = z.string().regex(/^user_\d+$/).brand<"UserId">();
type UserId = z.infer<typeof UserIdSchema>; CODE_BLOCK:
const UserIdSchema = z.string().regex(/^user_\d+$/).brand<"UserId">();
type UserId = z.infer<typeof UserIdSchema>; - Prevent Type Mix-ups: Avoid bugs caused by confusing UserId and PostId.
- Express Domain Models Clearly: Clearly express domain-specific types like currency, email addresses, and timestamps.
- Combine Runtime Validation with Type Safety: Write safe code by combining Zod's validation with type inference. - Zod Official Documentation: Branded types
- Zod GitHub Repository
- TypeScript Official Documentation: Type Compatibility
- Nominal typing - Wikipedia
how-totutorialguidedev.toaipythondatabasegitgithub