TypeScript Branded Types (UserId vs TaskId) and readonly Arrays
November 6, 2026
Welcome to episode 46 of TypeScript from Zero, a series for complete beginners (no JavaScript needed). Welcome back to TypeScript from zero! A user id and a task id are both numbers. But mixing them up is a bug, and tsc does not see it, because both are just numbers. Today a trick that fixes this, called branding. And at the end: arrays that can not be changed. A new file called brands.ts in the tasks project. Every code block below is the exact code and output from the video, ready to copy.
- All episodes are in the TypeScript from Zero playlist.
- Versions used: Node.js 26.13.0, npm 11.20.0, TypeScript 7.0.2.
Setup
We start in the terminal and open the project in VS Code:
cd tasks
touch src/brands.ts
code .1. Brand
A brand is a number with a secret label. Brand takes a type and a name, and makes the type together with an object that has one read only property, the brand. User id and task id are both numbers, but with different labels. The label exists only for tsc. It is never in the real value. To make one, we convert with as, and we hide that in one small function for each. The function load task wants a task id. We make a user id and a task id, and load the task: it works.
src/brands.ts:
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<number, "UserId">;
type TaskId = Brand<number, "TaskId">;
function userId(n: number): UserId {
return n as UserId;
}
function taskId(n: number): TaskId {
return n as TaskId;
}
function loadTask(id: TaskId): string {
return "task " + id;
}
const u = userId(1);
const t = taskId(1);
console.log(loadTask(t));$ node src/brands.ts
task 12. Mix up
Now the mistake: we give the user id to a function that wants a task id. tsc stops it: argument of type user id is not assignable to parameter of type task id. TS2345. Without the brands, this would have been a silent bug.
src/brands.ts:
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<number, "UserId">;
type TaskId = Brand<number, "TaskId">;
function userId(n: number): UserId {
return n as UserId;
}
function taskId(n: number): TaskId {
return n as TaskId;
}
function loadTask(id: TaskId): string {
return "task " + id;
}
const u = userId(1);
const t = taskId(1);
console.log(loadTask(t));
console.log(loadTask(u));$ npx tsc --noEmit
src/brands.ts:16:22 - error TS2345: Argument of type 'UserId' is not assignable to parameter of type 'TaskId'.
Type 'UserId' is not assignable to type '{ readonly __brand: "TaskId"; }'.
Types of property '__brand' are incompatible.
Type '"UserId"' is not assignable to type '"TaskId"'.
16 console.log(loadTask(u));
~
Found 1 error in src/brands.ts:163. Fix
We delete it. A plain number does not fit either: only a value that came through the task id function can be used.
src/brands.ts:
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<number, "UserId">;
type TaskId = Brand<number, "TaskId">;
function userId(n: number): UserId {
return n as UserId;
}
function taskId(n: number): TaskId {
return n as TaskId;
}
function loadTask(id: TaskId): string {
return "task " + id;
}
const u = userId(1);
const t = taskId(1);
console.log(loadTask(t));
console.log(loadTask(5));$ npx tsc --noEmit
src/brands.ts:16:22 - error TS2345: Argument of type 'number' is not assignable to parameter of type 'TaskId'.
Type 'number' is not assignable to type '{ readonly __brand: "TaskId"; }'.
16 console.log(loadTask(5));
~
Found 1 error in src/brands.ts:164. readonly
Now arrays. The type readonly string array is a list that nobody may change. We can read it, and ask for the length. But push does not even exist on this type. tsc says: property push does not exist on type readonly string array. TS2339.
src/brands.ts:
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<number, "UserId">;
type TaskId = Brand<number, "TaskId">;
function userId(n: number): UserId {
return n as UserId;
}
function taskId(n: number): TaskId {
return n as TaskId;
}
function loadTask(id: TaskId): string {
return "task " + id;
}
const u = userId(1);
const t = taskId(1);
console.log(loadTask(t));
const days: readonly string[] = ["Mon", "Tue"];
console.log(days.length);$ node src/brands.ts
task 1
2src/brands.ts:
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<number, "UserId">;
type TaskId = Brand<number, "TaskId">;
function userId(n: number): UserId {
return n as UserId;
}
function taskId(n: number): TaskId {
return n as TaskId;
}
function loadTask(id: TaskId): string {
return "task " + id;
}
const u = userId(1);
const t = taskId(1);
console.log(loadTask(t));
const days: readonly string[] = ["Mon", "Tue"];
console.log(days.length);
days.push("Wed");$ npx tsc --noEmit
src/brands.ts:18:6 - error TS2339: Property 'push' does not exist on type 'readonly string[]'.
18 days.push("Wed");
~~~~
Found 1 error in src/brands.ts:185. Parameters
We delete the line. Why is this useful? A function that only reads a list should say so, with readonly in the parameter. It promises not to change the list. And callers lose nothing: a normal array is accepted, because a normal array can always be used as read only. Total works with a plain list of numbers.
src/brands.ts:
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<number, "UserId">;
type TaskId = Brand<number, "TaskId">;
function userId(n: number): UserId {
return n as UserId;
}
function taskId(n: number): TaskId {
return n as TaskId;
}
function loadTask(id: TaskId): string {
return "task " + id;
}
const u = userId(1);
const t = taskId(1);
console.log(loadTask(t));
const days: readonly string[] = ["Mon", "Tue"];
console.log(days.length);
function total(xs: readonly number[]): number {
return xs.reduce((a, b) => a + b, 0);
}
console.log(total([1, 2, 3]));$ node src/brands.ts
task 1
2
6$ npx tsc --noEmitRecap
Today: branded types, to keep apart values that have the same shape, the as conversion hidden in one function, and read only arrays for safer functions. Next episode: Part 6, production, starting with tests.