Hluboký ponor do nejsilnějších typových vlastností TypeScriptu — podmíněné typy, mapované typy, klíčové slovo infer a šablonové literálové typy. Reálné patterny, které fakt použiješ.
Většina TypeScript vývojářů se zasekne na stejném místě. Znají interfaces, generika, union typy. Umí otypovat React komponentu. Umí zlikvidovat červené vlnovky. Ale typový systém berou jako nutné zlo — něco, co existuje proto, aby zabraňovalo bugům, ne jako nástroj, který jim aktivně pomáhá navrhovat lepší software.
Na tomhle plató jsem se držel asi dva roky. Ven mě vytáhlo zjištění, že typový systém TypeScriptu je sám o sobě programovací jazyk. Má podmínky, cykly, pattern matching, rekurzi a manipulaci s řetězci. Jakmile si tohle zvnitřníš, všechno se změní. Přestaneš bojovat s kompilátorem a začneš s ním spolupracovat.
Tenhle článek pokrývá typové vlastnosti, které neustále používám v produkčním kódu. Žádná akademická cvičení — reálné patterny, které mě zachránily před reálnými bugy.
Než se ponoříme do syntaxe, chci přerámovat způsob, jakým přemýšlíš o typech.
V programování na úrovni hodnot píšeš funkce, které transformují data za běhu:
function extractIds(users: User[]): number[] {
return users.map((u) => u.id);
}V programování na úrovni typů píšeš typy, které transformují jiné typy v době kompilace:
type ExtractIds<T extends { id: unknown }[]> = T[number]["id"];Mentální model je stejný — vstup, transformace, výstup. Rozdíl je v tom, že kód na úrovni typů běží při kompilaci, ne za běhu. Negeneruje žádný JavaScript. Jeho jediný účel je učinit nemožné stavy nereprezentovatelné.
Pokud jsi někdy napsal funkci, kde tři ze čtyř parametrů dávají smysl jen v určitých kombinacích, nebo kde návratový typ závisí na tom, co předáš, tak jsi programování na úrovni typů už potřeboval. Jen jsi možná nevěděl, že TypeScript to umí vyjádřit.
Podmíněné typy jsou if/else typového systému. Syntaxe vypadá jako ternární výraz:
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false
type C = IsString<string>; // trueKlíčové slovo extends tady neznamená dědičnost. Znamená „je přiřaditelný do." Představ si to jako „vejde se T do tvaru string?"
Pojďme přestavět některé vestavěné utility typy TypeScriptu, abychom pochopili, jak fungují.
NonNullable — odstraní null a undefined z union:
type MyNonNullable<T> = T extends null | undefined ? never : T;
type Test = MyNonNullable<string | null | undefined>;
// Výsledek: stringTyp never je „prázdná množina" — odstraňuje něco z union. Když se větev podmíněného typu vyhodnotí na never, ten člen union prostě zmizí.
Extract — ponechá pouze členy union, které odpovídají omezení:
type MyExtract<T, U> = T extends U ? T : never;
type Numbers = Extract<string | number | boolean, number | boolean>;
// Výsledek: number | booleanExclude — opak, odstraní odpovídající členy:
type MyExclude<T, U> = T extends U ? never : T;
type WithoutStrings = Exclude<string | number | boolean, string>;
// Výsledek: number | booleanTady to začíná být zajímavé — a kde se většina lidí ztrácí. Když předáš union typ podmíněnému typu, TypeScript distribuuje podmínku přes každého člena union zvlášť.
type ToArray<T> = T extends unknown ? T[] : never;
type Result = ToArray<string | number>;
// Mohl bys čekat: (string | number)[]
// Skutečný výsledek: string[] | number[]TypeScript nevyhodnocuje (string | number) extends unknown. Místo toho vyhodnotí string extends unknown a number extends unknown zvlášť a pak sjednotí výsledky.
Proto Extract a Exclude fungují tak, jak fungují. Distribuce se děje automaticky, když je T „nahý" typový parametr (není zabalený do ničeho).
Pokud chceš distribuci zabránit, zabal obě strany do tuple:
type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;
type Result = ToArrayNonDist<string | number>;
// Výsledek: (string | number)[]Tohle mě popálilo víckrát, než bych chtěl přiznat. Pokud tvůj podmíněný typ dává neočekávané výsledky s union vstupy, distribuce je téměř vždy důvod.
Tady je pattern, který používám při práci s API odpověďmi, které mohou buď uspět, nebo selhat:
type ApiResponse<T> = { status: "success"; data: T; error: never } | { status: "error"; data: never; error: string };
type ExtractData<R> = R extends { status: "success"; data: infer D } ? D : never;
type ExtractError<R> = R extends { status: "error"; error: infer E } ? E : never;
type UserResponse = ApiResponse<{ id: number; name: string }>;
type UserData = ExtractData<UserResponse>;
// Výsledek: { id: number; name: string }
type UserError = ExtractError<UserResponse>;
// Výsledek: stringTohle je čistější než ruční psaní type guardů pro každý tvar odpovědi.
inferinfer je pattern matching pro typy. Umožňuje deklarovat typovou proměnnou uvnitř podmíněného typu, kterou TypeScript sám odvodí.
Představ si to jako: „Vím, že tenhle typ má určitý tvar. Vytáhni z něj tu část, která mě zajímá."
type MyReturnType<T> = T extends (...args: unknown[]) => infer R ? R : never;
type Fn = (x: number, y: string) => boolean;
type Result = MyReturnType<Fn>; // booleanTypeScript vidí pattern (...args) => něco, porovná tvou funkci s ním a přiřadí návratový typ do R.
type MyParameters<T> = T extends (...args: infer P) => unknown ? P : never;
type Fn = (x: number, y: string) => void;
type Params = MyParameters<Fn>; // [x: number, y: string]Tohle používám pořád při práci s async funkcemi:
type UnpackPromise<T> = T extends Promise<infer U> ? U : T;
type A = UnpackPromise<Promise<string>>; // string
type B = UnpackPromise<Promise<number[]>>; // number[]
type C = UnpackPromise<string>; // string (passthrough)Pro hluboce vnořené promises:
type DeepUnpackPromise<T> = T extends Promise<infer U> ? DeepUnpackPromise<U> : T;
type Deep = DeepUnpackPromise<Promise<Promise<Promise<number>>>>;
// Výsledek: numbertype ElementOf<T> = T extends (infer E)[] ? E : never;
type Items = ElementOf<string[]>; // string
type Mixed = ElementOf<(string | number)[]>; // string | numberinfer v jednom typuMůžeš použít infer vícekrát k extrakci různých částí:
type FirstAndLast<T extends unknown[]> = T extends [infer First, ...unknown[], infer Last]
? { first: First; last: Last }
: never;
type Result = FirstAndLast<[1, 2, 3, 4]>;
// Výsledek: { first: 1; last: 4 }Tady je pattern, který používám v event-driven systémech:
type EventMap = {
"user:login": { userId: string; timestamp: number };
"user:logout": { userId: string };
"order:created": { orderId: string; total: number };
"order:shipped": { orderId: string; trackingId: string };
};
type EventName = keyof EventMap;
type EventPayload<E extends EventName> = EventMap[E];
// Extrakce všech user-related eventů
type UserEvents = Extract<EventName, `user:${string}`>;
// Výsledek: "user:login" | "user:logout"
// Získání payloadu pouze pro user eventy
type UserEventPayloads = EventPayload<UserEvents>;
// Výsledek: { userId: string; timestamp: number } | { userId: string }Mapované typy ti umožňují vytvářet nové objektové typy transformací každé vlastnosti existujícího typu. Jsou to map() typového systému.
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type Partial<T> = {
[K in keyof T]?: T[K];
};keyof T ti dá union všech klíčů vlastností. [K in ...] iteruje přes každý z nich. T[K] je typ dané vlastnosti.
Required — učiní všechny vlastnosti povinnými:
type MyRequired<T> = {
[K in keyof T]-?: T[K];
};Syntaxe -? odstraňuje volitelný modifikátor. Podobně -readonly odstraňuje readonly.
Pick — vybere konkrétní vlastnosti:
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
interface User {
id: number;
name: string;
email: string;
password: string;
}
type PublicUser = MyPick<User, "id" | "name" | "email">;
// { id: number; name: string; email: string }Omit — odstraní konkrétní vlastnosti:
type MyOmit<T, K extends keyof T> = {
[P in Exclude<keyof T, K>]: T[P];
};
type SafeUser = MyOmit<User, "password">;
// { id: number; name: string; email: string }Record — vytvoří objektový typ s konkrétními klíči a typy hodnot:
type MyRecord<K extends string | number | symbol, V> = {
[P in K]: V;
};
type StatusMap = MyRecord<"active" | "inactive" | "banned", boolean>;
// { active: boolean; inactive: boolean; banned: boolean }asTypeScript 4.1 zavedl přemapování klíčů, které ti umožňuje transformovat názvy vlastností:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
email: string;
}
type PersonGetters = Getters<Person>;
// {
// getName: () => string;
// getAge: () => number;
// getEmail: () => string;
// }Můžeš také filtrovat vlastnosti přemapováním na never:
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type StringProps = OnlyStrings<Person>;
// { name: string; email: string }Tohle je neuvěřitelně silné. Současně iteruješ přes vlastnosti, filtruješ je a transformuješ jejich názvy — to vše na úrovni typů.
Tenhle pattern jsem použil v několika knihovnách pro formuláře:
interface FormFields {
username: string;
email: string;
age: number;
bio: string;
}
type ValidationErrors<T> = {
[K in keyof T]?: string[];
};
type TouchedFields<T> = {
[K in keyof T]?: boolean;
};
type DirtyFields<T> = {
[K in keyof T]?: boolean;
};
type FormState<T> = {
values: T;
errors: ValidationErrors<T>;
touched: TouchedFields<T>;
dirty: DirtyFields<T>;
isValid: boolean;
isSubmitting: boolean;
};
// Teď je stav formuláře plně otypovaný na základě polí
type MyFormState = FormState<FormFields>;Přidej pole do FormFields a každý související typ se automaticky aktualizuje. Odstraň pole a kompilátor zachytí každou referenci. Tohle je přesně ten druh věcí, který zabraňuje celým kategoriím bugů.
Šablonové literálové typy ti umožňují manipulovat s řetězci na úrovni typů. Používají stejnou syntaxi s backticky jako JavaScript template literály, ale pro typy.
type Greeting = `Hello, ${string}`;
const a: Greeting = "Hello, world"; // OK
const b: Greeting = "Hello, TypeScript"; // OK
// const c: Greeting = "Hi, world"; // Chyba!TypeScript také poskytuje vestavěné typy pro manipulaci s řetězci:
type Upper = Uppercase<"hello">; // "HELLO"
type Lower = Lowercase<"HELLO">; // "hello"
type Cap = Capitalize<"hello">; // "Hello"
type Uncap = Uncapitalize<"Hello">; // "hello"Tady šablonové literálové typy opravdu vyniknou. Tady je pattern, který používám pro typově bezpečné event systémy:
type EventNames<T extends string> = `${T}Changed` | `${T}Deleted` | `${T}Created`;
type ModelEvents = EventNames<"user" | "order" | "product">;
// "userChanged" | "userDeleted" | "userCreated"
// | "orderChanged" | "orderDeleted" | "orderCreated"
// | "productChanged" | "productDeleted" | "productCreated"Devět eventů ze dvou typových parametrů. A všechny jsou typově bezpečné.
Šablonové literálové typy dokážou vynutit CSS patterny na úrovni typů:
type CSSUnit = "px" | "rem" | "em" | "vh" | "vw" | "%";
type CSSValue = `${number}${CSSUnit}`;
function setWidth(value: CSSValue): void {
// implementace
}
setWidth("100px"); // OK
setWidth("2.5rem"); // OK
setWidth("50%"); // OK
// setWidth("100"); // Chyba: neplatná CSS hodnota
// setWidth("big"); // ChybaŠablonové literálové typy můžeš použít s infer k parsování řetězců:
type ParseRoute<T extends string> = T extends `${infer _Start}:${infer Param}/${infer Rest}`
? Param | ParseRoute<`/${Rest}`>
: T extends `${infer _Start}:${infer Param}`
? Param
: never;
type RouteParams = ParseRoute<"/users/:userId/posts/:postId">;
// Výsledek: "userId" | "postId"Tohle je základ toho, jak fungují typově bezpečné routovací knihovny jako tRPC. Řetězec /users/:userId/posts/:postId je parsován na úrovni typů k extrakci názvů parametrů.
Tady se věci stávají opravdu mocnými:
type EventHandlers<T extends Record<string, unknown>> = {
[K in keyof T as `on${Capitalize<string & K>}Change`]: (newValue: T[K], oldValue: T[K]) => void;
};
interface ThemeConfig {
color: string;
fontSize: number;
darkMode: boolean;
}
type ThemeHandlers = EventHandlers<ThemeConfig>;
// {
// onColorChange: (newValue: string, oldValue: string) => void;
// onFontSizeChange: (newValue: number, oldValue: number) => void;
// onDarkModeChange: (newValue: boolean, oldValue: boolean) => void;
// }Z jednoduchého config rozhraní získáš plně otypované event handlery se správnými typy parametrů. Tohle je ten druh typového kódu, který reálně šetří vývojářský čas.
Tady je pokročilejší pattern — generování všech možných cest s tečkovou notací skrz vnořený objekt:
type DotPaths<T, Prefix extends string = ""> = T extends object
? {
[K in keyof T & string]: T[K] extends object
? `${Prefix}${K}` | DotPaths<T[K], `${Prefix}${K}.`>
: `${Prefix}${K}`;
}[keyof T & string]
: never;
interface Config {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
server: {
port: number;
};
}
type ConfigPaths = DotPaths<Config>;
// "database" | "database.host" | "database.port"
// | "database.credentials" | "database.credentials.username"
// | "database.credentials.password" | "server" | "server.port"Teď můžeš napsat funkci getConfig, která automaticky doplňuje každou možnou cestu a vrací správný typ pro každou z nich:
type GetValueByPath<T, P extends string> = P extends `${infer Key}.${infer Rest}`
? Key extends keyof T
? GetValueByPath<T[Key], Rest>
: never
: P extends keyof T
? T[P]
: never;
function getConfig<P extends ConfigPaths>(path: P): GetValueByPath<Config, P> {
// implementace
const keys = path.split(".");
let result: unknown = config;
for (const key of keys) {
result = (result as Record<string, unknown>)[key];
}
return result as GetValueByPath<Config, P>;
}
const host = getConfig("database.host"); // typ: string
const port = getConfig("database.port"); // typ: number
const username = getConfig("database.credentials.username"); // typ: stringPlné automatické doplňování, plná typová bezpečnost, nulový runtime overhead z typů.
TypeScript podporuje rekurzivní definice typů, což ti umožňuje pracovat s libovolně vnořenými strukturami.
Vestavěný Partial<T> dělá volitelné pouze vlastnosti na nejvyšší úrovni. Pro vnořené objekty potřebuješ rekurzi:
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? T[K] extends Array<infer U>
? Array<DeepPartial<U>>
: DeepPartial<T[K]>
: T[K];
};
interface Settings {
theme: {
colors: {
primary: string;
secondary: string;
};
fonts: {
heading: string;
body: string;
};
};
notifications: {
email: boolean;
push: boolean;
};
}
type PartialSettings = DeepPartial<Settings>;
// Každá vnořená vlastnost je teď volitelnáVšimni si kontroly pole: bez ní by se pole zpracovávala jako objekty a jejich číselné indexy by se staly volitelnými, což nechceš.
Stejný pattern, jiný modifikátor:
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? T[K] extends Function
? T[K]
: T[K] extends Array<infer U>
? ReadonlyArray<DeepReadonly<U>>
: DeepReadonly<T[K]>
: T[K];
};Kontrola Function je důležitá — bez ní by se na vlastnosti typu funkce aplikovalo readonly na jejich vnitřní strukturu, což nedává smysl.
Tady je klasický rekurzivní typ — reprezentace jakékoli platné JSON hodnoty:
type JsonPrimitive = string | number | boolean | null;
type JsonArray = JsonValue[];
type JsonObject = { [key: string]: JsonValue };
type JsonValue = JsonPrimitive | JsonArray | JsonObject;
function parseJson(input: string): JsonValue {
return JSON.parse(input);
}
function isJsonObject(value: JsonValue): value is JsonObject {
return typeof value === "object" && value !== null && !Array.isArray(value);
}Tohle je mnohem lepší než používat any pro parsování JSON. Říká to konzumentovi „dostaneš zpátky něco, ale musíš to zúžit, než to použiješ." Což je přesně pravda.
Praktický příklad — typování struktury navigačního menu:
interface MenuItem {
label: string;
href: string;
icon?: string;
children?: MenuItem[];
}
type Menu = MenuItem[];
const navigation: Menu = [
{
label: "Products",
href: "/products",
children: [
{
label: "Software",
href: "/products/software",
children: [
{ label: "IDE", href: "/products/software/ide" },
{ label: "CLI Tools", href: "/products/software/cli" },
],
},
{ label: "Hardware", href: "/products/hardware" },
],
},
{ label: "About", href: "/about" },
];Rekurzivní vlastnost children ti dává nekonečné vnořování s plnou typovou bezpečností.
Dovolte mi sdílet patterny, které jsem skutečně použil v produkčních systémech.
Builder pattern se stává výrazně užitečnějším, když typový systém sleduje, co bylo nastaveno:
type RequiredKeys = "host" | "port" | "database";
type BuilderState = {
[K in RequiredKeys]?: true;
};
class DatabaseConfigBuilder<State extends BuilderState = {}> {
private config: Partial<{
host: string;
port: number;
database: string;
ssl: boolean;
poolSize: number;
}> = {};
setHost(host: string): DatabaseConfigBuilder<State & { host: true }> {
this.config.host = host;
return this as unknown as DatabaseConfigBuilder<State & { host: true }>;
}
setPort(port: number): DatabaseConfigBuilder<State & { port: true }> {
this.config.port = port;
return this as unknown as DatabaseConfigBuilder<State & { port: true }>;
}
setDatabase(db: string): DatabaseConfigBuilder<State & { database: true }> {
this.config.database = db;
return this as unknown as DatabaseConfigBuilder<State & { database: true }>;
}
setSsl(ssl: boolean): DatabaseConfigBuilder<State> {
this.config.ssl = ssl;
return this as unknown as DatabaseConfigBuilder<State>;
}
setPoolSize(size: number): DatabaseConfigBuilder<State> {
this.config.poolSize = size;
return this as unknown as DatabaseConfigBuilder<State>;
}
build(
this: DatabaseConfigBuilder<{ host: true; port: true; database: true }>,
): Required<Pick<typeof this.config, "host" | "port" | "database">> &
Partial<Pick<typeof this.config, "ssl" | "poolSize">> {
return this.config as ReturnType<typeof this.build>;
}
}
// Tohle se zkompiluje:
const config = new DatabaseConfigBuilder().setHost("localhost").setPort(5432).setDatabase("myapp").setSsl(true).build();
// Tohle selže v době kompilace — chybí .setDatabase():
// const bad = new DatabaseConfigBuilder()
// .setHost("localhost")
// .setPort(5432)
// .build(); // Chyba: chybí 'database'Metoda build() se stane zavolatelnou teprve když jsou nastavena všechna povinná pole. Tohle se zachytí v době kompilace, ne za běhu.
Tady je event emitter, kde jsou typy payloadů vynucené:
type EventMap = Record<string, unknown>;
class TypedEventEmitter<Events extends EventMap> {
private listeners: {
[K in keyof Events]?: Array<(payload: Events[K]) => void>;
} = {};
on<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(listener);
}
emit<K extends keyof Events>(event: K, payload: Events[K]): void {
this.listeners[event]?.forEach((listener) => listener(payload));
}
off<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): void {
const handlers = this.listeners[event];
if (handlers) {
this.listeners[event] = handlers.filter((h) => h !== listener) as typeof handlers;
}
}
}
// Použití
interface AppEvents {
"user:login": { userId: string; timestamp: number };
"user:logout": { userId: string };
"notification:new": { message: string; level: "info" | "warning" | "error" };
}
const emitter = new TypedEventEmitter<AppEvents>();
// Plně typované — IDE automaticky doplňuje názvy eventů a tvary payloadů
emitter.on("user:login", (payload) => {
console.log(payload.userId); // string
console.log(payload.timestamp); // number
});
// Typová chyba: payload neodpovídá
// emitter.emit("user:login", { userId: "123" });
// Chyba: chybí 'timestamp'
// Typová chyba: event neexistuje
// emitter.on("user:signup", () => {});
// Chyba: "user:signup" není v AppEventsTenhle pattern jsem použil ve třech různých produkčních projektech. Zachytává celé kategorie event-related bugů v době kompilace.
Diskriminované unie jsou pravděpodobně nejužitečnější pattern v TypeScriptu. V kombinaci s vyčerpávající kontrolou garantují, že obsloužíš každý případ:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return 0.5 * shape.base * shape.height;
default:
// Tenhle řádek zajišťuje vyčerpávající kontrolu
const _exhaustive: never = shape;
return _exhaustive;
}
}Pokud někdo přidá novou variantu tvaru (řekněme "pentagon"), tahle funkce se nezkompiluje, protože never nemůže být přiřazena hodnota. Kompilátor tě donutí obsloužit každý případ.
Jdu ještě dál s helper funkcí:
function assertNever(value: never, message?: string): never {
throw new Error(message ?? `Unexpected value: ${JSON.stringify(value)}`);
}
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return 0.5 * shape.base * shape.height;
default:
assertNever(shape, `Unknown shape kind: ${(shape as Shape).kind}`);
}
}Diskriminované unie také krásně modelují stavové automaty:
type ConnectionState =
| { status: "disconnected" }
| { status: "connecting"; attempt: number }
| { status: "connected"; socket: WebSocket; connectedAt: Date }
| { status: "error"; error: Error; lastAttempt: Date };
type ConnectionAction =
| { type: "CONNECT" }
| { type: "CONNECTED"; socket: WebSocket }
| { type: "DISCONNECT" }
| { type: "ERROR"; error: Error }
| { type: "RETRY" };
function connectionReducer(state: ConnectionState, action: ConnectionAction): ConnectionState {
switch (state.status) {
case "disconnected":
if (action.type === "CONNECT") {
return { status: "connecting", attempt: 1 };
}
return state;
case "connecting":
if (action.type === "CONNECTED") {
return {
status: "connected",
socket: action.socket,
connectedAt: new Date(),
};
}
if (action.type === "ERROR") {
return {
status: "error",
error: action.error,
lastAttempt: new Date(),
};
}
return state;
case "connected":
if (action.type === "DISCONNECT") {
state.socket.close();
return { status: "disconnected" };
}
if (action.type === "ERROR") {
return {
status: "error",
error: action.error,
lastAttempt: new Date(),
};
}
return state;
case "error":
if (action.type === "RETRY") {
return { status: "connecting", attempt: 1 };
}
if (action.type === "DISCONNECT") {
return { status: "disconnected" };
}
return state;
default:
assertNever(state, "Unknown connection state");
}
}Každý stav má pouze vlastnosti, které dávají smysl pro daný stav. Nemůžeš přistoupit k socket, když jsi odpojený. Nemůžeš přistoupit k error, když jsi připojený. Typový systém vynucuje omezení stavového automatu.
Ještě jeden pattern, který považuji za nezbytný — použití branded typů k prevenci záměny hodnot, které náhodou sdílejí stejný podkladový typ:
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
type Email = Brand<string, "Email">;
function createUserId(id: string): UserId {
return id as UserId;
}
function createOrderId(id: string): OrderId {
return id as OrderId;
}
function getUser(id: UserId): Promise<User> {
// ...
}
function getOrder(id: OrderId): Promise<Order> {
// ...
}
const userId = createUserId("user_123");
const orderId = createOrderId("order_456");
getUser(userId); // OK
// getUser(orderId); // Chyba! OrderId nelze přiřadit k UserIdJak UserId, tak OrderId jsou za běhu řetězce. Ale v době kompilace jsou to odlišné typy. Doslova nemůžeš předat order ID tam, kde se očekává user ID. Tohle zachytilo reálné bugy v každém projektu, kde jsem to použil.
Pokročilé typy jsou mocné, ale přichází s pastmi. Tady je to, co jsem se naučil těžkým způsobem.
TypeScript má limit hloubky rekurze pro typy (aktuálně kolem 50 úrovní, i když se to liší). Pokud jdeš příliš hluboko, dostaneš obávanou chybu „Type instantiation is excessively deep and possibly infinite".
// Tohle narazí na limit rekurze u hluboce vnořených objektů
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
// Oprava: Přidej čítač hloubky
type DeepPartialSafe<T, Depth extends number[] = []> = Depth["length"] extends 10
? T
: {
[K in keyof T]?: T[K] extends object ? DeepPartialSafe<T[K], [...Depth, 0]> : T[K];
};Trik s čítačem hloubky používá tuple, který roste s každým rekurzivním krokem. Když dosáhne tvého limitu, rekurze se zastaví.
Složité typy mohou výrazně zpomalit tvůj editor a build časy. Viděl jsem projekty, kde jeden přehnaně chytrý typ přidal 3 sekundy ke zpětné vazbě při každém stisku klávesy.
Varovné signály:
tsc --noEmit trvá výrazně déle po přidání typu// Tohle je moc chytré — generuje kombinatorickou explozi
type AllCombinations<T extends string[]> = T extends [infer First extends string, ...infer Rest extends string[]]
? `${First}${AllCombinations<Rest>}` | AllCombinations<Rest>
: "";
// Nedělej tohle s více než 5-6 elementyTohle je možná nejdůležitější sekce. Pokročilé typy by se měly používat, když zabraňují bugům nebo zlepšují vývojářský zážitek. Neměly by se používat k předvádění.
Nepoužívej je, když:
Record<string, unknown> by stačilPoužívej je, když:
// Přehnané — nedělej tohle pro jednoduchý config
type Config = DeepReadonly<DeepRequired<Merge<DefaultConfig, Partial<UserConfig>>>>;
// Prostě udělej tohle
interface Config {
readonly host: string;
readonly port: number;
readonly debug: boolean;
}Mé pravidlo: pokud vysvětlení typu trvá déle než vysvětlení bugu, kterému brání, zjednoduš ho.
Když složitý typ nefunguje, používám tenhle helper k „vidění" toho, co TypeScript vyřešil:
// Rozbalí typ pro inspekci v IDE hover tooltipech
type Prettify<T> = {
[K in keyof T]: T[K];
} & {};
// Použij k debugování
type Problematic = SomeComplexType<Input>;
type Debug = Prettify<Problematic>;
// Teď najeď myší nad Debug v IDE a uvidíš vyřešený typTrik & {} donutí TypeScript dychtivě vyhodnotit typ místo zobrazení typového aliasu. Ušetřil mi hodiny zmatků.
Další technika — izoluj a testuj postupně:
// Místo debugování všeho najednou:
type Final = StepThree<StepTwo<StepOne<Input>>>;
// Rozděl to:
type AfterStep1 = StepOne<Input>; // najeď myší pro kontrolu
type AfterStep2 = StepTwo<AfterStep1>; // najeď myší pro kontrolu
type AfterStep3 = StepThree<AfterStep2>; // najeď myší pro kontroluT extends U ? X : Y) jsou if/else pro typy. Pozor na distributivní chování s uniony.infer je pattern matching — používej k extrakci typů ze struktur jako signatury funkcí, promises a pole.{ [K in keyof T]: ... }) iterují přes vlastnosti. Kombinuj s as pro přemapování a filtrování klíčů.Typový systém TypeScriptu je Turingovsky úplný, což znamená, že s ním můžeš udělat téměř cokoli. Umění je vědět, kdy bys měl.