TypeScript'in en güçlü tip özelliklerine derinlemesine bakış — conditional type'lar, mapped type'lar, infer anahtar kelimesi ve template literal type'lar. Gerçekten kullanacağın pratik kalıplar.
Çoğu TypeScript geliştiricisi aynı noktada takılıp kalır. Interface'leri, generic'leri, union type'ları bilirler. Bir React bileşenini tipleyebilirler. Kırmızı dalgalı çizgileri ortadan kaldırabilirler. Ama tip sistemini zorunlu bir kötülük olarak görürler — hata önlemek için var olan, aktif olarak daha iyi yazılım tasarlamalarına yardımcı olmayan bir şey.
Ben o düzlükte yaklaşık iki yıl kaldım. Beni oradan çıkaran şey, TypeScript'in tip sisteminin kendi başına bir programlama dili olduğunu fark etmemdi. Koşulları, döngüleri, desen eşleme, özyineleme ve string manipülasyonu var. Bunu bir kez içselleştirdiğinde her şey değişiyor. Derleyiciyle savaşmayı bırakıp onunla iş birliği yapmaya başlıyorsun.
Bu yazı, üretim kodunda sürekli kullandığım tip-seviyesi özellikleri kapsıyor. Akademik alıştırmalar değil — beni gerçek hatalardan kurtaran gerçek kalıplar.
Sözdizime dalmadan önce, tipler hakkındaki düşünce biçimini yeniden çerçevelemek istiyorum.
Değer-seviyesi programlamada, çalışma zamanında veriyi dönüştüren fonksiyonlar yazarsın:
function extractIds(users: User[]): number[] {
return users.map(u => u.id);
}Tip-seviyesi programlamada ise derleme zamanında diğer tipleri dönüştüren tipler yazarsın:
type ExtractIds<T extends { id: unknown }[]> = T[number]["id"];Zihinsel model aynı — girdi, dönüşüm, çıktı. Fark şu ki tip-seviyesi kod derleme sırasında çalışır, çalışma zamanında değil. Sıfır JavaScript üretir. Tek amacı imkansız durumları temsil edilemez kılmaktır.
Eğer daha önce dört parametreden üçünün sadece belirli kombinasyonlarda anlamlı olduğu veya dönüş tipinin girdiye bağlı olduğu bir fonksiyon yazdıysan, zaten tip-seviyesi programlamaya ihtiyaç duymuşsundur. Sadece TypeScript'in bunu ifade edebildiğini bilmiyor olabilirsin.
Conditional type'lar, tip sisteminin if/else'idir. Sözdizimi ternary operatörüne benzer:
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false
type C = IsString<string>; // trueBuradaki extends anahtar kelimesi kalıtım anlamına gelmiyor. "Atanabilir mi?" anlamına geliyor. Bunu "T, string şekline sığar mı?" olarak düşün.
TypeScript'in yerleşik yardımcı tiplerinden bazılarını yeniden inşa ederek nasıl çalıştıklarını anlayalım.
NonNullable — bir union'dan null ve undefined'ı kaldırır:
type MyNonNullable<T> = T extends null | undefined ? never : T;
type Test = MyNonNullable<string | null | undefined>;
// Sonuç: stringnever tipi "boş küme"dir — bir union'dan bir şeyi kaldırır. Bir conditional type dalı never'a çözümlendiğinde, union'un o üyesi basitçe kaybolur.
Extract — yalnızca bir kısıtlamayla eşleşen union üyelerini tutar:
type MyExtract<T, U> = T extends U ? T : never;
type Numbers = Extract<string | number | boolean, number | boolean>;
// Sonuç: number | booleanExclude — tam tersi, eşleşen üyeleri kaldırır:
type MyExclude<T, U> = T extends U ? never : T;
type WithoutStrings = Exclude<string | number | boolean, string>;
// Sonuç: number | booleanİşte işlerin ilginçleştiği ve çoğu insanın kafasının karıştığı yer. Bir conditional type'a union type geçirdiğinde, TypeScript koşulu union'un her üyesine ayrı ayrı dağıtır.
type ToArray<T> = T extends unknown ? T[] : never;
type Result = ToArray<string | number>;
// Beklediğin: (string | number)[]
// Gerçek sonuç: string[] | number[]TypeScript (string | number) extends unknown'ı değerlendirmez. Bunun yerine string extends unknown ve number extends unknown'ı ayrı ayrı değerlendirir, sonra sonuçları birleştirir.
Extract ve Exclude'un bu şekilde çalışmasının nedeni budur. Dağıtım, T "çıplak" bir tip parametresi olduğunda (hiçbir şeye sarılmadığında) otomatik olarak gerçekleşir.
Dağıtımı engellemek istiyorsan, her iki tarafı da bir tuple'a sar:
type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;
type Result = ToArrayNonDist<string | number>;
// Sonuç: (string | number)[]Bundan itiraf etmek istediğimden daha fazla darbe yedim. Conditional type'ın union girdileriyle beklenmedik sonuçlar veriyorsa, neredeyse her zaman nedeni dağıtımdır.
İşte başarılı veya başarısız olabilen API yanıtlarıyla uğraşırken kullandığım bir kalıp:
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>;
// Sonuç: { id: number; name: string }
type UserError = ExtractError<UserResponse>;
// Sonuç: stringBu, her yanıt şekli için manuel tip korumaları yazmaktan çok daha temiz.
infer Anahtar Kelimesi#infer, tipler için desen eşlemedir. Bir conditional type içinde TypeScript'in senin için çözeceği bir tip değişkeni tanımlamanı sağlar.
Bunu şöyle düşün: "Bu tipin belirli bir şekli olduğunu biliyorum. Umursadığım kısmı çıkar."
type MyReturnType<T> = T extends (...args: unknown[]) => infer R ? R : never;
type Fn = (x: number, y: string) => boolean;
type Result = MyReturnType<Fn>; // booleanTypeScript (...args) => bir şey kalıbını görür, fonksiyonunu buna karşı eşler ve dönüş tipini R'ye atar.
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]Bunu async fonksiyonlarla çalışırken sürekli kullanıyorum:
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 (doğrudan geçiş)Derin iç içe geçmiş promise'ler için:
type DeepUnpackPromise<T> = T extends Promise<infer U>
? DeepUnpackPromise<U>
: T;
type Deep = DeepUnpackPromise<Promise<Promise<Promise<number>>>>;
// Sonuç: numbertype ElementOf<T> = T extends (infer E)[] ? E : never;
type Items = ElementOf<string[]>; // string
type Mixed = ElementOf<(string | number)[]>; // string | numberinfer#Farklı parçaları çıkarmak için infer'i birden fazla kez kullanabilirsin:
type FirstAndLast<T extends unknown[]> =
T extends [infer First, ...unknown[], infer Last]
? { first: First; last: Last }
: never;
type Result = FirstAndLast<[1, 2, 3, 4]>;
// Sonuç: { first: 1; last: 4 }İşte olay güdümlü sistemlerde kullandığım bir kalıp:
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];
// Tüm kullanıcı ile ilgili olayları çıkar
type UserEvents = Extract<EventName, `user:${string}`>;
// Sonuç: "user:login" | "user:logout"
// Sadece kullanıcı olaylarının payload'ını al
type UserEventPayloads = EventPayload<UserEvents>;
// Sonuç: { userId: string; timestamp: number } | { userId: string }Mapped type'lar, mevcut bir tipin her özelliğini dönüştürerek yeni nesne tipleri oluşturmanı sağlar. Tip sisteminin map()'idir.
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type Partial<T> = {
[K in keyof T]?: T[K];
};keyof T tüm özellik anahtarlarının bir union'ını verir. [K in ...] her birini yineler. T[K] o özelliğin tipidir.
Required — tüm özellikleri zorunlu yap:
type MyRequired<T> = {
[K in keyof T]-?: T[K];
};-? sözdizimi opsiyonel niteleyiciyi kaldırır. Benzer şekilde, -readonly salt okunur niteleyiciyi kaldırır.
Pick — belirli özellikleri seç:
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 — belirli özellikleri kaldır:
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 — belirli anahtarlar ve değer tipleriyle bir nesne tipi oluştur:
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 }as ile Anahtar Yeniden Eşleme#TypeScript 4.1, özellik adlarını dönüştürmenize izin veren anahtar yeniden eşlemeyi tanıttı:
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;
// }Ayrıca never'a eşleyerek özellikleri filtreleyebilirsin:
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type StringProps = OnlyStrings<Person>;
// { name: string; email: string }Bu inanılmaz güçlü. Aynı anda özellikleri yineliyorsun, filtreliyorsun ve adlarını dönüştürüyorsun — hepsi tip seviyesinde.
Bu kalıbı birkaç form kütüphanesinde kullandım:
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;
};
// Şimdi form durumun tamamen alan bazında tiplenmiş
type MyFormState = FormState<FormFields>;FormFields'a bir alan ekle ve ilgili her tip otomatik güncellenir. Bir alanı kaldır ve derleyici her referansı yakalar. Bu, tüm hata kategorilerini önleyen türden bir şey.
Template literal type'lar, stringleri tip seviyesinde manipüle etmeni sağlar. JavaScript template literal'larıyla aynı backtick sözdizimini kullanır, ama tipler için.
type Greeting = `Hello, ${string}`;
const a: Greeting = "Hello, world"; // OK
const b: Greeting = "Hello, TypeScript"; // OK
// const c: Greeting = "Hi, world"; // Hata!TypeScript ayrıca yerleşik string manipülasyon tipleri sağlar:
type Upper = Uppercase<"hello">; // "HELLO"
type Lower = Lowercase<"HELLO">; // "hello"
type Cap = Capitalize<"hello">; // "Hello"
type Uncap = Uncapitalize<"Hello">; // "hello"Template literal type'ların gerçekten parladığı yer burası. İşte tip-güvenli olay sistemleri için kullandığım bir kalıp:
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"İki tip parametresinden dokuz olay. Ve hepsi tip-güvenli.
Template literal type'lar, CSS kalıplarını tip seviyesinde zorunlu kılabilir:
type CSSUnit = "px" | "rem" | "em" | "vh" | "vw" | "%";
type CSSValue = `${number}${CSSUnit}`;
function setWidth(value: CSSValue): void {
// implementasyon
}
setWidth("100px"); // OK
setWidth("2.5rem"); // OK
setWidth("50%"); // OK
// setWidth("100"); // Hata: geçerli bir CSS değeri değil
// setWidth("big"); // HataTemplate literal type'ları infer ile birlikte stringleri ayrıştırmak için kullanabilirsin:
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">;
// Sonuç: "userId" | "postId"Bu, tRPC gibi tip-güvenli yönlendirme kütüphanelerinin temelini oluşturur. /users/:userId/posts/:postId stringi, parametre adlarını çıkarmak için tip seviyesinde ayrıştırılır.
İşte gerçekten güçlü hale geldiği yer:
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;
// }Basit bir config interface'inden, doğru parametre tipleriyle tam tiplenmiş olay işleyicileri elde edersin. Bu, gerçekten geliştirme süresinden tasarruf sağlayan türden tip-seviyesi koddur.
İşte daha ileri bir kalıp — iç içe geçmiş bir nesne boyunca tüm olası nokta-notasyonu yollarını oluşturmak:
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"Şimdi her olası yolu otomatik tamamlayan ve her biri için doğru tipi döndüren bir getConfig fonksiyonu yazabilirsin:
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> {
// implementasyon
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"); // tip: string
const port = getConfig("database.port"); // tip: number
const username = getConfig("database.credentials.username"); // tip: stringTam otomatik tamamlama, tam tip güvenliği, tiplerden kaynaklanan sıfır çalışma zamanı yükü.
TypeScript özyinelemeli tip tanımlarını destekler, bu da keyfi derinlikte iç içe geçmiş yapıları işlemeni sağlar.
Yerleşik Partial<T> yalnızca üst düzey özellikleri opsiyonel yapar. İç içe geçmiş nesneler için özyinelemeye ihtiyacın var:
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>;
// Her iç içe geçmiş özellik artık opsiyonelDizi kontrolüne dikkat et: bu olmazsa, diziler nesne olarak ele alınır ve sayısal indeksleri opsiyonel olur, ki bu istediğin şey değil.
Aynı kalıp, farklı niteleyici:
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];
};Function kontrolü önemli — bu olmazsa, fonksiyon özellikleri iç yapılarına readonly uygulanır, ki bu mantıklı değil.
İşte klasik bir özyinelemeli tip — herhangi bir geçerli JSON değerini temsil etmek:
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);
}Bu, JSON ayrıştırma için any kullanmaktan çok daha iyi. Tüketiciye "bir şey geri alacaksın, ama kullanmadan önce daraltman gerekiyor" der. Ki tam olarak gerçek budur.
Pratik bir örnek — navigasyon menü yapısını tiplemek:
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" },
];Özyinelemeli children özelliği, tam tip güvenliğiyle sonsuz iç içe geçme sağlar.
Üretim sistemlerinde gerçekten kullandığım kalıplardan bazılarını paylaşayım.
Builder kalıbı, tip sistemi nelerin ayarlandığını takip ettiğinde çok daha kullanışlı hale gelir:
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>;
}
}
// Bu derlenir:
const config = new DatabaseConfigBuilder()
.setHost("localhost")
.setPort(5432)
.setDatabase("myapp")
.setSsl(true)
.build();
// Bu derleme zamanında başarısız olur — .setDatabase() eksik:
// const bad = new DatabaseConfigBuilder()
// .setHost("localhost")
// .setPort(5432)
// .build(); // Hata: 'database' eksikbuild() metodu, yalnızca tüm zorunlu alanlar ayarlandığında çağrılabilir hale gelir. Bu, çalışma zamanında değil, derleme zamanında yakalanır.
İşte payload tiplerinin zorunlu kılındığı bir olay yayıcı:
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;
}
}
}
// Kullanım
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>();
// Tam tiplenmiş — IDE olay adlarını ve payload şekillerini otomatik tamamlar
emitter.on("user:login", (payload) => {
console.log(payload.userId); // string
console.log(payload.timestamp); // number
});
// Tip hatası: payload eşleşmiyor
// emitter.emit("user:login", { userId: "123" });
// Hata: 'timestamp' eksik
// Tip hatası: olay mevcut değil
// emitter.on("user:signup", () => {});
// Hata: "user:signup" AppEvents'te yokBu kalıbı üç farklı üretim projesinde kullandım. Tüm olay ile ilgili hata kategorilerini derleme zamanında yakalar.
Ayrıştırılmış union'lar muhtemelen TypeScript'teki en kullanışlı tek kalıp. Kapsamlı kontrol ile birleştirildiğinde, her durumu ele aldığını garanti eder:
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:
// Bu satır kapsamlı kontrolü sağlar
const _exhaustive: never = shape;
return _exhaustive;
}
}Birisi yeni bir şekil varyantı eklerse (diyelim ki "pentagon"), bu fonksiyon derlenmeyecektir çünkü never'a bir değer atanamaz. Derleyici her durumu ele almayı zorlar.
Bunu bir yardımcı fonksiyonla daha da ileri götürüyorum:
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}`);
}
}Ayrıştırılmış union'lar, durum makinelerini de güzelce modeller:
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");
}
}Her durum, yalnızca o durum için anlamlı olan özelliklere sahip. Bağlantı kesildiğinde socket'e erişemezsin. Bağlıyken error'a erişemezsin. Tip sistemi, durum makinesinin kısıtlamalarını zorlar.
Vazgeçilmez bulduğum bir kalıp daha — aynı temel tipi paylaşan değerlerin birbirine karışmasını önlemek için markalı tipler kullanmak:
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); // Hata! OrderId, UserId'ye atanamazHem UserId hem de OrderId çalışma zamanında string'dir. Ama derleme zamanında ayrı tiplerdir. Bir kullanıcı ID'si beklenen yere sipariş ID'si geçirmek kelimenin tam anlamıyla imkansız. Bu, kullandığım her projede gerçek hataları yakaladı.
İleri seviye tipler güçlüdür, ama tuzaklarla birlikte gelir. İşte zor yoldan öğrendiklerim.
TypeScript'in tipler için özyineleme derinlik sınırı vardır (şu anda yaklaşık 50 seviye, ancak değişir). Çok derine inersen, korkulan "Type instantiation is excessively deep and possibly infinite" hatasını alırsın.
// Bu, derin iç içe geçmiş nesneler için özyineleme limitine ulaşacak
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
// Düzeltme: Derinlik sayacı ekle
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];
};Derinlik sayacı hilesi, her özyinelemeli adımda büyüyen bir tuple kullanır. Limitine ulaştığında, özyineleme durur.
Karmaşık tipler editörünü ve derleme sürelerini önemli ölçüde yavaşlatabilir. Tek bir aşırı zekice tipin her tuş vuruşunun geri bildirim döngüsüne 3 saniye eklediği projeler gördüm.
Uyarı işaretleri:
tsc --noEmit bir tip ekledikten sonra fark edilir şekilde daha uzun sürüyor// Bu çok akıllıca — kombinatoryal patlama oluşturuyor
type AllCombinations<T extends string[]> = T extends [
infer First extends string,
...infer Rest extends string[]
]
? `${First}${AllCombinations<Rest>}` | AllCombinations<Rest>
: "";
// 5-6'dan fazla elemanla bunu yapmaBu belki de en önemli bölüm. İleri seviye tipler, hataları önlediğinde veya geliştirici deneyimini iyileştirdiğinde kullanılmalıdır. Gösteriş yapmak için kullanılmamalıdır.
Bunları şu durumlarda kullanma:
Record<string, unknown> yeterli olacaksaBunları şu durumlarda kullan:
// Aşırı mühendislik — basit bir config için bunu yapma
type Config = DeepReadonly<
DeepRequired<
Merge<DefaultConfig, Partial<UserConfig>>
>
>;
// Sadece bunu yap
interface Config {
readonly host: string;
readonly port: number;
readonly debug: boolean;
}Temel kuralım: tipi açıklamak, önlediği hatayı açıklamaktan daha uzun sürüyorsa, basitleştir.
Karmaşık bir tip çalışmadığında, TypeScript'in neye çözdüğünü "görmek" için bu yardımcıyı kullanıyorum:
// IDE hover tooltip'lerinde inceleme için bir tipi genişletir
type Prettify<T> = {
[K in keyof T]: T[K];
} & {};
// Hata ayıklamak için kullan
type Problematic = SomeComplexType<Input>;
type Debug = Prettify<Problematic>;
// Şimdi IDE'de Debug'ın üzerine gel ve çözümlenmiş tipi gör& {} hilesi, TypeScript'i tip takma adını göstermek yerine tipi istekli bir şekilde değerlendirmeye zorlar. Beni saatlerce kafa karışıklığından kurtardı.
Bir başka teknik — izole et ve kademeli olarak test et:
// Bunun hepsini tek seferde hata ayıklamak yerine:
type Final = StepThree<StepTwo<StepOne<Input>>>;
// Parçalara ayır:
type AfterStep1 = StepOne<Input>; // kontrol etmek için üzerine gel
type AfterStep2 = StepTwo<AfterStep1>; // kontrol etmek için üzerine gel
type AfterStep3 = StepThree<AfterStep2>; // kontrol etmek için üzerine gelT extends U ? X : Y) tipler için if/else'dir. Union'larla dağıtıcı davranışa dikkat et.infer desen eşlemedir — fonksiyon imzaları, promise'ler ve diziler gibi yapılardan tipleri çıkarmak için kullan.{ [K in keyof T]: ... }) özellikler üzerinde yineleme yapar. Anahtar yeniden eşleme ve filtreleme için as ile birleştir.TypeScript tip sistemi Turing-complete'tir, yani onunla neredeyse her şeyi yapabilirsin. Sanat, ne zaman yapman gerektiğini bilmektir.