This episode covers designing types that stay easy to maintain as the project grows: modeling domains with discriminated unions, brand types for distinguishing identical values, composition, type file organization, and the principle of minimizing any while maximizing unknown.

In a small project, any type feels good enough. In a large project with dozens of developers, type design determines development speed. Poorly designed types become an obstacle; well-designed types become documentation that guides everyone to write correct code.
Scalable means types stay expressive without becoming complicated. You don't want a confusing giant union, or types that force constant conversions because the source of truth was designed wrong at the start.
Episode 19 covers patterns proven in large codebases: modeling domains with discriminated unions, distinguishing same-shaped values with brand types, simple composition, consistent file organization, and the discipline of avoiding any.
The discriminated union from episode 11 becomes the foundation for modeling domains. Its advantage appears when used consistently as a single state type:
type KeadaanPesan =
| { status: "mengirim"; progress: number }
| { status: "terkirim"; id: string }
| { status: "gagal"; alasan: string };
function deskripsi(k: KeadaanPesan): string {
switch (k.status) {
case "mengirim":
return `Mengirim ${k.progress} persen`;
case "terkirim":
return `Terkirim dengan id ${k.id}`;
case "gagal":
return `Gagal: ${k.alasan}`;
}
}Each union member carries the data relevant to its state. No optional fields pile up, no invalid combination can occur. The compiler forces every state to be handled, and the model grows by adding union members alone.
The power of this pattern: the whole application uses one state type, then each module derives the view it needs. Changes to the domain model are automatically checked across every usage. This is how you add features without tearing apart many files.
TypeScript uses structural typing: two types with the same shape are considered the same. Sometimes that's dangerous:
type IDPengguna = string & { readonly brand: unique symbol };
type IDOrder = string & { readonly brand: unique symbol };
function cariUser(id: IDPengguna): void {}The declaration type IDPengguna = string & { readonly brand: unique symbol } creates a type structurally distinct from a plain string and from IDOrder. The cariUser function only accepts values deliberately marked as a user ID, so it can't be confused with an order ID or an arbitrary string.
The rule: use brand types when two values from different domains have the same shape, like IDs of two entities or different units. Without this, swapped values are only caught in production.
Build types from small pieces that are combined:
interface DapatDibuat {
dibuat: Date;
}
interface DapatDiubah {
diubah: Date;
}
type CatatanLengkap = DapatDibuat & DapatDiubah & {
isi: string;
};Types are split into small, reusable agreements. Adding a dibuat property to any entity is just an intersection. Composition avoids duplication and makes new types cheap to create.
Consistent placement helps scale:
src/
types/
domain.ts
api.ts
shared.ts
index.tsA types folder holds domain definitions and API contracts in a central place, exported through a single index.ts barrel. Other modules import from one door. These clear boundaries keep types from spreading to unexpected places and make dependency traces easy to map.
Scalable types start with discipline: any is a failure point that spreads.
function ambilData(): unknown {
return fetchDataKasar();
}An unknown return forces the caller to validate or narrow before using the value. Unlike any, which frees everything right away. In episode 22 you'll see compiler options that enforce this discipline automatically.
Warning
When you find any in a codebase, the right treatment isn't to add more any, but to narrow at one entry point and spread the now-certain type. A single any can pollute other types that depend on it.
Episode 19 teaches type design that grows with the project: discriminated unions to model domains, brand types to distinguish similar values, organized composition and folders, and the discipline of minimizing any.
Key takeaways:
unknown forces validation; any releases every guarantee.In the next episode 20 we'll discuss build output, bundling, and source maps — controlling the compilation result from tsc up to production artifacts.