7 Az Bilinen TypeScript Özelliği: satisfies, Branded Types ve Daha Fazlası
Production kodunu iyileştiren 7 az bilinen TypeScript özelliği: satisfies, noUncheckedIndexedAccess, branded types, discriminated unions ve daha fazlası.
tsconfig.json içinde strict açmak, type güvenliği meselesinin kapandığı hissini verir. Kapanmaz. Indexed access hâlâ undefined ihtimalini gizleyen bir type döndürür, bir UserID hâlâ OrderID bekleyen fonksiyona sorunsuz geçer, union’a eklenen yeni bir member da önceden yazdığın her switch bloğundan sessizce kaçar.
En yüksek getirili değişiklik noUncheckedIndexedAccess: strict modun açmadığı bir compiler flag’i. Önce onu ekle, ardından geri kalan boşlukları kapatan type seviyesindeki araçlara geç: satisfies, branded types, exhaustiveness kontrollü discriminated unions, type predicates, template literal types ve infer. Yedisi de yalnızca derleme zamanında var olur; runtime’a hiçbir maliyet çıkarmaz.
Strict Mode’un Kapatmadığı Boşluklar#
Her yerde any: any kullanmak TypeScript’in amacını boşa çıkarır. Gizlenen type hataları runtime’a kadar fark edilmez.
Kontrolsüz array erişimi: array[5] ifadesi undefined dönebilir, TypeScript’in varsayılan konfigürasyonu ise bunu hiç uyarmaz; strict mode açıkken bile.
Structural typing karışıklığı: TypeScript structural typing kullandığı için UserID ve OrderID (her ikisi de number) birbirinin yerine geçebilir. ID’ler karıştığında veri sessizce bozulur.
Eksik union kapsaması: Production’da karşılıksız case’ler ortaya çıkar, çünkü bir state type’ına eklenen yeni case mevcut switch bloklarına dokunmaz.
Zayıf sistem sınırları: API’lerden gelen veri runtime validation gerektirir. Validation mantığı ile type narrowing arasındaki bağ genellikle net değildir.
Konfigürasyonda type kaybı: Configuration object’leri üzerinde type assertion kullanmak, hataları yakalayabilecek type bilgisini siler.
İç içe type çıkarımı: Karmaşık generic type’lar elle type çıkarımı gerektirir; bu da zamanla tekrara ve sapmaya dönüşür.
Yedi Özellik#
1. satisfies Operator ile Const Assertions#
satisfies operator (TypeScript 4.9+), type validation’ı precise literal type inference ile birleştirir - hem compile-time checking hem de spesifik type’lar sağlar. Configuration object’leri bir type schema’ya karşı validation gerektirir, ama as type assertion kullanmak yol boyunca literal type bilgisini kaybettirir.
// satisfies olmadan - type bilgisi kaybolur
const routes = {
home: { path: '/', methods: ['GET', 'POST'] },
api: { path: '/api', methods: ['POST'] }
} as const;
// routes.home.path '/' (iyi), ama validation yok
as const satisfies ikisini birden geri kazandırır: immutability ve validation.
type Route = {
path: string;
methods: readonly ('GET' | 'POST' | 'PUT' | 'DELETE')[];
};
type Routes = Record<string, Route>;
const routes = {
home: { path: '/', methods: ['GET', 'POST'] },
api: { path: '/api', methods: ['POST'] },
// Comment'i kaldırırsan TypeScript error:
// invalid: { path: '/bad', methods: ['INVALID'] }
} as const satisfies Routes;
// Şimdi: type-checked VE precise literal types
routes.home.path; // type: '/' (literal, string değil)
routes.api.methods; // type: readonly ['POST']
Bir HTTP method’undaki yazım hatası ya da eksik bir path, isteği beklemeden build’i düşürür. Hata, object literal içindeki ilgili anahtarı doğrudan işaret eder; büyük bir config dosyasında işe yarayan kısım da bu.
2. noUncheckedIndexedAccess Compiler Flag’i#
Kritik bir konfigürasyon detayı: noUncheckedIndexedAccess compiler option’ı strict mode’a dahil değil, ancak “Cannot read property of undefined” hatalarının tüm bir sınıfını önlüyor.
Array ve object indexed access undefined dönebilir, ama TypeScript’in default davranışı bu gerçeği yansıtmıyor.
// tsconfig.json - default strict mode
{
"compilerOptions": {
"strict": true
}
}
// Bu kod güvenli görünüyor ama değil
const users = ['Alice', 'Bob'];
const user = users[5]; // type: string (YANLIŞ - aslında undefined!)
user.toUpperCase(); // Runtime error: Cannot read property 'toUpperCase' of undefined
noUncheckedIndexedAccess’i explicit olarak enable etmek bunu doğrudan düzeltir:
// tsconfig.json - production-ready strict mode
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true // Bunu ekle!
}
}
// Şimdi TypeScript gerçeği yansıtıyor
const users = ['Alice', 'Bob'];
const user = users[5]; // type: string | undefined (DOĞRU)
// TypeScript undefined'ı handle etmeni zorluyor
if (user) {
user.toUpperCase(); // Güvenli
}
// Ya da optional chaining kullan
const upperName = users[5]?.toUpperCase();
Bu option’ı mevcut bir codebase’de açmak, guard’sız her indeks erişimi için bir hata üretir; yani sayı, kodun ne kadar array işi yaptığıyla orantılı büyür. Çoğu mekanik düzeltmedir: bir ?., bir uzunluk kontrolü ya da varsayılan değerli bir destructure. Tek seferde bitir; dosya dosya ilerlersen flag bir süre sonra yine kapatılır. Bu option strict mode’un parçası da değil, bu yüzden birçok geliştirici var olduğunu bilmiyor.
3. Nominal Type Safety için Branded Types#
TypeScript structural typing kullanır, yani aynı structure’a sahip iki type birbirinin yerine kullanılabilir. Bu güçlü olsa da, nominal typing davranışı istediğinde subtle bug’lara yol açabilir.
// Problem
type UserID = number;
type OrderID = number;
function getUser(id: UserID) { /* ... */ }
function getOrder(id: OrderID) { /* ... */ }
const userId: UserID = 123;
const orderId: OrderID = 456;
getUser(orderId); // TypeScript buna izin veriyor (KÖTÜ!)
Multi-tenant bir sistemde bu hata kendini belli etmez. Sorgu yanlış tenant üzerinde sorunsuz çalışır; log’da exception görünmez, sonuç sızan veri olarak ortaya çıkar.
Branded type’lar bu boşluğu kapatır: type level’da nominal benzeri davranış yaratırlar.
// Generic brand utility
type Brand<K, T> = K & { readonly __brand: T };
type UserID = Brand<number, 'UserID'>;
type OrderID = Brand<number, 'OrderID'>;
// Branded value'lar yaratmak için constructor function'lar
const UserID = (id: number): UserID => id as UserID;
const OrderID = (id: number): OrderID => id as OrderID;
const userId = UserID(123);
const orderId = OrderID(456);
getUser(orderId); // BAD: TypeScript error: Type 'OrderID' is not assignable to type 'UserID'
Üstüne bir validation function eklemek garantiyi tamamlar:
type Email = Brand<string, 'Email'>;
const Email = (value: string): Email => {
if (!value.includes('@') || !value.includes('.')) {
throw new Error('Invalid email format');
}
return value as Email;
};
// Artık email variable'ları validate edildiği garanti
function sendEmail(to: Email) {
// Tekrar validate etmeye gerek yok - type system garanti ediyor
}
4. Exhaustiveness Checking ile Discriminated Unions#
State machine’ler ve API response’ları, never type üzerinden exhaustiveness checking ile birleştirilmiş discriminated union’lardan büyük fayda sağlar.
Tüm case’leri handle etmeyen switch statement’lar runtime failure’lara yol açar:
type Result<T> =
| { status: 'success'; data: T }
| { status: 'error'; error: Error }
| { status: 'loading' };
// Exhaustiveness checking olmadan
function handleResult<T>(result: Result<T>) {
switch (result.status) {
case 'success':
return result.data;
case 'error':
throw result.error;
// 'loading' case'ini unuttuk - error yok!
}
// Loading state için undefined döner - bug!
}
never type’ı exhaustiveness’i zorlar:
function handleResult<T>(result: Result<T>) {
switch (result.status) {
case 'success':
return result.data;
case 'error':
throw result.error;
case 'loading':
return null;
default:
// Bu TypeScript'i tüm case'leri kontrol etmeye zorlar
const exhaustive: never = result;
throw new Error(`Unhandled case: ${exhaustive}`);
}
}
// Yeni bir status eklemek compilation'ı bozar
type Result<T> =
| { status: 'success'; data: T }
| { status: 'error'; error: Error }
| { status: 'loading' }
| { status: 'cancelled' }; // TypeScript şimdi handleResult'ta error verir
Yeni bir union member eklediğinde, örneğin bir API client’a 'retry' state’i eklendiğinde, TypeScript güncellenmesi gereken her konumu anında işaretler; böylece tüm çağrı noktalarını tek tek taramana gerek kalmaz.
5. Type Predicates vs Assertion Functions#
TypeScript, type narrowing için iki pattern sunar: type predicates ve assertion functions. Her birini ne zaman kullanacağını anlamak, temiz ve type-safe validation mantığı için kritik.
Type predicate boolean döner ve conditional check’lerde iyi çalışır:
function isString(value: unknown): value is string {
return typeof value === 'string';
}
// Conditional'larda kullan
const data: unknown = getSomeData();
if (isString(data)) {
data.toUpperCase(); // data burada string
}
Assertion function throw eder veya void döner; scope’un geri kalanı için type’ı daraltır:
function assertString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error('Not a string');
}
}
// Validation için kullan
const data: unknown = getSomeData();
assertString(data); // string değilse throw eder
data.toUpperCase(); // data scope'un geri kalanında string
Aynı pattern kendi domain object’ini validate etmeye de uzanır:
type User = {
id: number;
email: string;
name: string;
};
function assertUser(obj: unknown): asserts obj is User {
if (
typeof obj !== 'object' ||
obj === null ||
!('id' in obj) ||
!('email' in obj) ||
!('name' in obj) ||
typeof obj.id !== 'number' ||
typeof obj.email !== 'string' ||
typeof obj.name !== 'string'
) {
throw new Error('Invalid user object');
}
}
// API response validation
async function fetchUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data: unknown = await response.json();
assertUser(data); // structure'ı validate eder
return data; // TypeScript bunun User olduğunu biliyor
}
Optional check’lerde, filter işlemlerinde ve conditional mantıkta predicate’e; mandatory validation, parse function’larında ve sistem sınırlarındaki guard clause’larda ise assertion’a yönel. En kritik nokta şu: assertion function’lar failure’da throw etmeli, false dönmemeli; false dönmek type’ı hiçbir zaman daraltmaz.
6. String Pattern’ler için Template Literal Types#
Template literal types (TypeScript 4.1+), type level’da type-safe string manipulation sağlar; düz bir string type’ının kaçırdığı pattern hatalarını yakalar.
CSS Unit Types:
type CSSUnit = 'px' | 'em' | 'rem' | '%';
type CSSValue<T extends string> = `${number}${T}`;
type Padding = CSSValue<CSSUnit>;
const padding: Padding = '10px'; // const invalid: Padding = '10abc'; // BAD: Error
Event Handler Naming:
type EventName = 'click' | 'focus' | 'blur' | 'hover';
type EventHandler<T extends EventName> = `on${Capitalize<T>}`;
type ClickHandler = EventHandler<'click'>; // 'onClick'
type FocusHandler = EventHandler<'focus'>; // 'onFocus'
type Handlers = {
[K in EventName as EventHandler<K>]: (event: Event) => void;
};
// Generates: { onClick: ..., onFocus: ..., onBlur: ..., onHover: ... }
API Route Typing:
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Endpoint = '/users' | '/posts' | '/comments';
type Route = `${HTTPMethod} ${Endpoint}`;
const route: Route = 'GET /users'; // const invalid: Route = 'GET /invalid'; // BAD: Error
// Type-safe route matcher
function matchRoute(route: Route): void {
// TypeScript route'un valid olduğunu biliyor
}
Path Parameter Extraction (advanced):
type ExtractParams<T extends string> =
T extends `${infer _Start}:${infer Param}/${infer Rest}`
? { [K in Param | keyof ExtractParams<`/${Rest}`>]: string }
: T extends `${infer _Start}:${infer Param}`
? { [K in Param]: string }
: {};
type UserRoute = '/users/:userId/posts/:postId';
type Params = ExtractParams<UserRoute>; // { userId: string; postId: string }
function getPost(params: Params) {
console.log(params.userId, params.postId); // Type-safe
// console.log(params.invalid); // BAD: Error
}
7. Type Extraction için infer Keyword#
infer keyword, kompleks generic structure’lardan type’lar extract etmeni sağlar, güçlü type-level programming’e olanak tanır.
Promise değerinin type’ını çıkarma:
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<number>; // number
// Async function'ları typing etmek için yararlı
declare function fetchData(): Promise<{ id: number; name: string }>;
type Data = UnwrapPromise<ReturnType<typeof fetchData>>;
// { id: number; name: string }
Array eleman type’ını çıkarma:
type ElementType<T> = T extends (infer U)[] ? U : T;
type Items = ElementType<string[]>; // string
type Single = ElementType<number>; // number
// Generic array utility'leri için yararlı
function first<T extends any[]>(arr: T): ElementType<T> | undefined {
return arr[0];
}
Type-Safe API Client:
type APIResponse = {
'/users': { id: number; name: string }[];
'/posts': { id: number; title: string; body: string }[];
'/comments': { id: number; text: string; authorId: number }[];
};
type FetchResult<T extends keyof APIResponse> = APIResponse[T];
async function fetchAPI<T extends keyof APIResponse>(
endpoint: T
): Promise<FetchResult<T>> {
const res = await fetch(endpoint);
return res.json();
}
// Type-safe kullanım
const users = await fetchAPI('/users');
// type: { id: number; name: string }[]
const posts = await fetchAPI('/posts');
// type: { id: number; title: string; body: string }[]
Deep Partial Utility:
type DeepPartial<T> = T extends object
? { [P in keyof T]?: DeepPartial<T[P]> }
: T;
type Config = {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
};
type PartialConfig = DeepPartial<Config>;
// Tüm property'ler recursive olarak optional
const config: PartialConfig = {
database: {
credentials: {
username: 'admin'
// password optional
}
// host ve port optional
}
};
Mevcut Bir Projede Devreye Alma#
Temel Konfigürasyon#
Yukarıda geçen flag’lerin tamamını açan, production’a hazır bir tsconfig.json:
{
"compilerOptions": {
// Standard strict flag'ler
"strict": true,
// Ek strictness (strict mode'da DEĞİL!)
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"exactOptionalPropertyTypes": true,
// Modern module handling (TS 5.0+)
"verbatimModuleSyntax": true,
"moduleDetection": "force",
// Modern JavaScript target et
"target": "ES2022",
"lib": ["ES2022", "DOM"],
// Daha iyi import'lar
"esModuleInterop": true,
"resolveJsonModule": true,
"allowJs": true,
// Performance
"skipLibCheck": true,
"incremental": true,
// Kullanılmayan kod tespiti
"noUnusedLocals": true,
"noUnusedParameters": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false
}
}
Geçiş Planı#
Sırasıyla üç aşama:
Aşama 1: strict mode’u aç
- Önce
noImplicitAny’ye odaklan:any’yiunknownya da doğru type’la değiştir - Aynı geçişte çözemediğin case’ler için geçici olarak
// @ts-expect-errorkullan
Aşama 2: noUncheckedIndexedAccess’i ekle
- Düzeltmelerin çoğu
?.optional chaining veyaif (arr[i])guard’ı eklemek - Her hatayı susturmadan önce oku. Bir kısmı gerçek bir bug’ı işaret ediyor
Aşama 3: Type seviyesindeki pattern’leri benimse (süreklilik ister)
- Kritik domain identifier’lar için branded type’lar tanıt
- Switch statement’ları discriminated unions + exhaustiveness ile değiştir
- Configuration object’leri için
satisfieskullan - Kod refactor edildikçe gradual adoption
Performans#
En büyük yükü derleme zamanı taşıyor: ek flag’ler type checking’e iş ekler ve bu maliyet, pattern’leri ne kadar kullandığından çok codebase’in büyüklüğüne bağlı artar. Yavaş olduğuna karar vermeden önce kendi projende ölç; incremental ve skipLibCheck yükün çoğunu emiyor. Runtime’a etkisi sıfır, çünkü tüm type bilgisi compilation sırasında siliniyor. Asıl izlenmesi gereken editör tepkisi: yavaşlayan kısım satisfies ya da branded type’lar değil, derinlemesine recursive conditional type’lar; language server takılmaya başlarsa önce infer zincirlerine bak, sonra codebase’i project reference’larla böl.
Type Assertion’ların, Indexing’in ve infer Zincirlerinin Kırıldığı Yerler#
Validasyonu atlayan type assertion’lar
as ile type assertion type checking’i tamamen bypass eder.
// Kötü - validation yok
const user = response as User;
// İyi - önce validate et
function isUser(obj: unknown): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
'email' in obj
);
}
const user = isUser(response) ? response : null;
noUncheckedIndexedAccess’in var olduğunu unutmak
strict: true ile bile, bu option’ı explicit enable etmedikçe indexed access güvenli değil.
Kontrolden çıkan infer chain’leri
Aşırı kompleks type utility’leri maintain etmek zor olur. Bunları net comment’lerle daha küçük, named type’lara böl.
// Okunması zor
type Complex<T> = T extends { a: infer A extends { b: infer B } } ? B : never;
// Daha iyi - net isimlerle parçala
type ExtractA<T> = T extends { a: infer A } ? A : never;
type ExtractB<T> = T extends { b: infer B } ? B : never;
type Result<T> = ExtractB<ExtractA<T>>;
Hangi Özelliği Ne Zaman Kullanmalı#
| Özellik | En İyi Kullanım | Ne Zaman Kullanma |
|---|---|---|
satisfies | Config object’leri, const data | Dynamic runtime data |
noUncheckedIndexedAccess | Tüm projeler (default olmalı) | Yoğun array access’li legacy kod |
| Branded types | Domain ID’leri, validate edilmiş string’ler | Sistemler arası sık dönüşüm |
| Discriminated unions | State machine’ler, API response’ları | Basit binary state’ler (boolean kullan) |
| Template literals | String pattern’leri, type-safe key’ler | Kompleks parsing mantığı |
infer | Library kodu, tekrar kullanılabilir utility’ler | Tek kullanımlık type manipulation’lar |
| Type assertions | Validate edilmiş external data | Internal kod (proper type’lar kullan) |
Çoğu proje için varsayılan şu: önce noUncheckedIndexedAccess’i aç, ardından yanlış bir değerin veriyi bozacağı ya da bir state’in atlanacağı yerlerde branded type’ları ve exhaustive union’ları devreye al. Maliyetin faydayı aştığı yerlerde bu varsayılandan sap. Sıkı döngülerde array indeksleyen eski bir modül, flag açıkken bug’dan çok gürültü üretir; tek seferlik bir type dönüşümü de elle yazılmış bir infer zincirini hak etmez.
Kaynaklar#
- TypeScript El Kitabı (yeni sekmede açılır) - TypeScript dil özelliklerini kapsayan resmi rehber
- Narrowing (yeni sekmede açılır) - Type predicate’leri, discriminated union’ları ve
neverile exhaustiveness kontrolünü anlatan el kitabı bölümü - Yardımcı Tipler (yeni sekmede açılır) - Yerleşik utility type yardımcıları için resmi referans
- Koşullu Tipler (yeni sekmede açılır) - Koşullu tipleri ve
inferanahtar kelimesini anlatan el kitabı bölümü - Şablon Literal Tipler (yeni sekmede açılır) - Tip düzeyinde string manipülasyonu için el kitabı bölümü
- TSConfig: noUncheckedIndexedAccess (yeni sekmede açılır) - Flag’in indexed access tiplerine ne eklediğini açıklayan compiler option referansı
- TypeScript 4.9 Sürüm Notları (yeni sekmede açılır) -
satisfiesoperatörünü ve onu doğuran örnekleri tanıtan sürüm notu - TypeScript 4.1 Sürüm Notları (yeni sekmede açılır) - Template literal tipleri ve mapped type’larda key remapping’i tanıtan sürüm notu
- TypeScript 3.7 Sürüm Notları (yeni sekmede açılır) - Assertion function’ları ve
assertsdönüş tipi sözdizimini tanıtan sürüm notu
İlgili yazılar
SOLID prensiplerinin modern JavaScript'te uygulanışı: TypeScript, React hooks ve fonksiyonel pattern'lerle pratik örnekler, ayrıca ne zaman gereksiz.
typescript · javascript · react +4
CDK stack düzeni için yaşam döngüsü testi: bir kaynağın ömrü tek bir dağıtımdan uzunsa kendi uzun ömürlü stack'ine koyun, ona bilinen bir adla erişin.
aws-cdk · infrastructure-as-code · typescript +3
Modern TypeScript linting ve formatlama araçlarının karşılaştırması: ESLint, Prettier, Biome ve Oxlint için performans ölçümleri, konfigürasyon ve migration.
typescript · code-quality · developer-experience +2
AWS CDK projelerinde service-based, domain-based, feature-based ve layer-based organizasyon patternlerini karar çerçeveleri ve örneklerle ne zaman seçeceğini öğren.
aws-cdk · typescript · infrastructure-as-code +3
Factory function'lar, higher-order function'lar ve composition ile AWS CDK'yı type-safe, tekrar kullanılabilir bir infrastructure toolkit'e dönüştürmek.
aws-cdk · typescript · infrastructure-as-code +2