TypeScript'te tip güvenliği nerede biter?
Derleyici geçti diye program doğru olmuyor. any yazmadan da tip sistemine yalan söyleyebileceğin beş yer, strict'in yakalamadıkları ve deliklerin sınırda nasıl kapatıldığı.
TypeScript'in ne olduğunu anlatan yazı çok. Bu yazı başka bir soruyu soruyor:
strict: true yazdın, any kullanmadın, derleyici geçti — programın doğru
mu?
Değil. Ve nerede olmadığını bilmek, tip sistemini gerçekten kullanmakla ona gereğinden fazla güvenmek arasındaki fark.
Temel gerçek: tipler çalışma anında yok
TypeScript derlendiğinde tipler silinir. Tarayıcıya ya da Node'a inen şey düz JavaScript; hiçbir tip kontrolü çalışma anına taşınmaz.
type User = { id: number; name: string };
function greet(user: User) {
return `Merhaba ${user.name}`;
}Bu fonksiyon derlendiğinde geriye şu kalıyor:
function greet(user) {
return `Merhaba ${user.name}`;
}Yani greet'e çalışma anında null gelirse hiçbir şey onu durdurmuyor.
TypeScript'in verdiği güvence şu: kodun içinden yanlış çağrı yapamazsın.
Kodun dışından gelen her şey — API yanıtı, form verisi, localStorage, ortam
değişkeni — bu güvencenin dışında.
Aşağıdaki beş delik hep aynı yerden çıkıyor.
Delik 1: as bir kontrol değil, bir iddia
En sık ve en tehlikeli olanı bu.
const res = await fetch("/api/user");
const user = (await res.json()) as User;
console.log(user.name.toUpperCase());Burada hiçbir doğrulama yok. as User demek "bunun User olduğunu ben
söylüyorum" demek. API { error: "not found" } dönerse derleyici hiçbir şey
demiyor, satır çalışma anında Cannot read properties of undefined ile
patlıyor.
Yalnız derleyicinin bilemeyeceği bir şeyi sen gerçekten biliyorsan:
document.getElementById("x") as HTMLInputElement gibi. Dışarıdan gelen
veride as kullanıyorsan, doğrulama yapmıyorsun — doğrulama yaptığını
sanıyorsun.
Delik 2: JSON.parse her şeye any diyor
const raw = localStorage.getItem("settings");
const settings = JSON.parse(raw!); // any
settings.theme.colors.primary; // derleyici sessizJSON.parse'ın dönüş tipi any. any de tip sisteminin kapalı olduğu yer
demek: any üzerinden yapılan her şey kontrolsüz geçiyor ve bu, atandığı yere
kadar yayılıyor.
fetch yanıtı da aynı: res.json() tipi Promise<any>.
JSON.parse'ın sonucunu unknown olarak al. unknown da her şeyi kabul
ediyor ama kullanmadan önce daraltmanı zorunlu kılıyor:
const settings: unknown = JSON.parse(raw ?? "{}");
// settings.theme → derleme hatası, önce doğrulaman gerekiyorDelik 3: ! operatörü
Ünlem işareti "bu null ya da undefined değil" demek — yine bir iddia.
const el = document.querySelector(".modal")!;
el.classList.add("open"); // element yoksa çalışma anında patlar
const user = users.find((u) => u.id === id)!;İkinci satır özellikle sinsi: find sonucu bulunamazsa undefined ama !
bunu susturuyor. Doğrusu kontrol etmek:
const user = users.find((u) => u.id === id);
if (!user) {
return notFound();
}Delik 4: strict dizi indekslemesini yakalamıyor
Bunu deneyerek doğruladım. Şu kod strict: true altında hatasız geçiyor:
const arr: string[] = [];
const first = arr[0];
console.log(first.length); // çalışma anında: Cannot read properties of undefinedTypeScript arr[0]'ın tipini string sayıyor, oysa dizi boş. Aynı şey
Record<string, T> erişiminde de geçerli.
Çözüm strict'in içinde değil, ayrı bir bayrakta:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true
}
}Aynı kod artık hata veriyor: 'first' is possibly 'undefined'.
Bayrağı açtığında mevcut kodda çok sayıda hata çıkabilir — ama çıkan her hata, gerçekten var olan bir çalışma anı riskini gösteriyor.
strict: true bir paket, ama hepsini kapsamıyor. Şunlar ayrıca açılıyor:
noUncheckedIndexedAccess— dizi/nesne indekslemesi| undefinedalırexactOptionalPropertyTypes—{ a?: string }alanına açıkçaundefinedatamayı engellernoImplicitOverride— sınıftaoverrideyazmayı zorunlu kılarnoPropertyAccessFromIndexSignature— index imzasına nokta ile erişimi engeller
İlk ikisi yeni bir projede baştan açılmalı; sonradan açmak göç işi.
Delik 5: process.env ve yapılandırma
const dbUrl = process.env.DATABASE_URL; // string | undefined
const client = new Client({ url: dbUrl! }); // yine iddiaOrtam değişkeni bir kod değil, bir dağıtım ayrıntısı. Değişken sunucuda tanımlanmamışsa hata en kötü yerde çıkıyor: uygulamanın açılışında değil, o kod yolunun ilk çalıştığı anda. Doğrusu açılışta bir kez doğrulamak:
function requireEnv(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`Eksik ortam değişkeni: ${name}`);
}
return value;
}
export const env = {
databaseUrl: requireEnv("DATABASE_URL"),
resendApiKey: requireEnv("RESEND_API_KEY"),
};Uygulama eksik yapılandırmayla açılmıyor — hata, sebebini söyleyerek ve doğru zamanda çıkıyor.
Delikleri sınırda kapatmak
Beş deliğin ortak noktası şu: hepsi verinin sisteme girdiği yerde. O yüzden çözüm de tek: sınırlarda çalışma anı doğrulaması yap, içeride tiplere güven.
Zod bunu tek adımda çözüyor — hem doğruluyor hem de TypeScript tipini şemadan türetiyor, yani tipi iki kez yazmıyorsun:
import { z } from "zod";
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
// Tip şemadan türüyor — ayrıca `type User` yazmıyorsun
type User = z.infer<typeof UserSchema>;
export async function getUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`);
const json: unknown = await res.json();
// Burada gerçekten kontrol ediliyor; `as` gibi bir iddia değil
return UserSchema.parse(json);
}Bu satırdan sonra user.name'in string olduğunu biliyorsun, varsaymıyorsun.
Aynı yaklaşım form verisi, webhook gövdesi ve localStorage için de geçerli.
İlgili yazı: Next.js'te form validasyonu: client ve server side
Eskimiş iki alışkanlık
React.FC kullanma. Bir dönem standarttı; artık gereksiz. Props'u doğrudan
yazmak hem daha kısa hem generic bileşenlerde sorun çıkarmıyor:
// Eski
const Button: React.FC<ButtonProps> = ({ children }) => { /* ... */ };
// Şimdi
function Button({ children }: ButtonProps) { /* ... */ }enum yerine as const düşün. enum derlendiğinde çalışma anında kod
üretiyor — tipler siliniyor ama enum kalıyor. Union tipi ise hiçbir şey
üretmiyor:
// Çalışma anında nesne üretir
enum LogLevel { DEBUG = "debug", INFO = "info" }
// Hiçbir şey üretmez, aynı işi görür
const LOG_LEVELS = ["debug", "info", "warn", "error"] as const;
type LogLevel = (typeof LOG_LEVELS)[number];"const enum daha performanslı" tavsiyesi dolaşıyor, ama bir kısıtı var:
isolatedModules açıkken — ki Next.js bunu zorunlu tutuyor — bir .d.ts
içindeki ambient const enum'a erişmek hata veriyor:
TS2748: Cannot access ambient const enums when 'isolatedModules' is enabled.
Kendi dosyanda tanımlayıp import ettiğin const enum çalışıyor; kütüphaneden gelen ambient olan çalışmıyor. Bu ayrımı bilmiyorsan hatanın nereden geldiğini bulmak zaman alıyor.
Özet
- Tipler çalışma anında yok; güvence yalnız kendi kodunun içinde
asve!birer iddia; dış veride kullanılırsa doğrulama yapılmamış demektirJSON.parseveres.json()anydöner —unknownolarak al, daraltmaya zorlastrictdizi indekslemesini yakalamıyor;noUncheckedIndexedAccessayrıca açılmalı- Ortam değişkenlerini açılışta bir kez doğrula
- Doğrulamayı sınıra koy (Zod), içeride tiplere güven
React.FCveenumalışkanlıklarını gözden geçir
TypeScript bir kanıtlayıcı değil, bir iletişim aracı. Ne bildiğini kodun içine yazmanı sağlıyor — ama ona yalan söylersen sessizce inanıyor. İşin zor kısmı tip yazmak değil, yalan söylemediğinden emin olmak.
Bu yazıyı faydalı bulduysan paylaşabilirsin.