İçeriğe geç
academia.sh

Ders 13 / 23

Tip Daraltma

Denetim akışı çözümlemesiyle tip daraltma; typeof, in ve instanceof koruyucuları, doğruluk daraltmasının tuzağı, tip yüklemleri, doğrulama fonksiyonları ve daraltmanın kaybolduğu durumlar.

İçindekiler

Önceki derslerde birkaç kez bir birleşim tipi üzerinde sınama yapıldı ve derleyici tipi tek bir üyeye indirdi. Bu indirgemenin kuralları henüz sıralanmadı.

Daraltma (narrowing), derleyicinin bir değerin tipini denetim akışındaki sınamalara bakarak küçültmesidir. Arkasındaki mekanizmaya denetim akışı çözümlemesi (control flow analysis) denir: derleyici, programın her noktasında hangi koşulların kanıtlanmış olduğunu izler. Bu ders hangi sınamaların kanıt sayıldığını, kendi doğrulamanızın derleyiciye nasıl bildirileceğini ve kanıtın nerede kaybolduğunu ele alır.

Yerleşik Koruyucular

Tip koruyucusu (type guard), bir değerin tipini daraltan sınamadır. En yaygını typeof işlecidir:

function bicimle(deger: string | number | null): string {
  if (deger === null) {
    return "-";
  }
  if (typeof deger === "number") {
    return deger.toFixed(1);
  }
  return deger.trim();
}

console.log(bicimle(21.4), bicimle("  s-01  "), bicimle(null));

Çıktı 21.4 s-01 -tir.

Üç dal, üç daraltma adımıdır. deger === null sınaması eşitlikle daraltır; ilk return sonrası kalan tip string | numberdır. typeof deger === "number" sınamasından sonra gövdede tip number olur ve toFixed çağrılabilir. Son satıra ulaşıldığında kalan tek olasılık stringtir; trim çağrısı için ek bir sınama gerekmez.

Son adım dikkat ister: derleyici erken dönüşleri izler. return deyimiyle biten bir dal, sonraki kodda o olasılığı eler. Programlama Temelleri kursunda tanıtılan koruma cümlesi kalıbı, tip düzeyinde de kazanç sağlar.

Nesne tipleri için iki koruyucu daha vardır. in işleci alan varlığına bakar:

type SayisalOlcum = { kimlik: string; deger: number };
type MetinselOlcum = { kimlik: string; metin: string };

function yaz(kayit: SayisalOlcum | MetinselOlcum): string {
  if ("deger" in kayit) {
    return kayit.deger.toFixed(1);
  }
  return kayit.metin.trim();
}

console.log(yaz({ kimlik: "s-01", deger: 21.4 }));
console.log(yaz({ kimlik: "s-02", metin: "  bozuk  " }));

Çıktı:

21.4
bozuk

instanceof işleci ise sınıf örneklerini ayırır:

class DosyaKaynagi {
  constructor(readonly yol: string) {}
}

class AgKaynagi {
  constructor(readonly adres: string) {}
}

function tanim(kaynak: DosyaKaynagi | AgKaynagi): string {
  if (kaynak instanceof DosyaKaynagi) {
    return `dosya: ${kaynak.yol}`;
  }
  return `ag: ${kaynak.adres}`;
}

console.log(tanim(new DosyaKaynagi("/veri/olcum.jsonl")));
console.log(tanim(new AgKaynagi("10.0.0.7:9000")));

Çıktı:

dosya: /veri/olcum.jsonl
ag: 10.0.0.7:9000

Bu üçünün ortak özelliği, hepsinin çalışma zamanında gerçekten çalışan sınamalar olmasıdır. Daraltma, tip sisteminin bir varsayımı değil; koddaki bir sınamanın tip düzeyine yansımasıdır.

Doğruluk Daraltmasının Tuzağı

Bir değerin doğruluk değerine bakmak da daraltır, ancak beklenenden fazlasını eler:

function etiket(deger: number | undefined): string {
  if (!deger) {
    return "olcum yok";
  }
  return deger.toFixed(1);
}

console.log(etiket(21.4));
console.log(etiket(0));
console.log(etiket(undefined));

Çıktı:

21.4
olcum yok
olcum yok

Bu dosya tip denetiminden geçer, ama ikinci çıktı yanlıştır: 0 geçerli bir ölçümdür. JavaScript Temelleri kursunda kurulan yanlış sayılan değerler listesi burada devreye girer — 0, "" ve NaN da yanlış sayılır ve !deger sınaması onları da eler.

Tip sistemi bu hatayı yakalamaz, çünkü daraltma doğru yapılmıştır: !deger bloğundan sonra tip gerçekten numberdır. Hata tipte değil, sınamanın seçiminde.

Kural: undefined ve null için eşitlik sınaması yazılır. if (deger === undefined) ya da if (deger == null) — ikincisi hem null hem undefined durumunu eler ve bu, iki eşittirin savunulabilir tek kullanımıdır.

Tip Yüklemleri

Doğrulama ayrı bir fonksiyona taşındığında daraltma kaybolur: derleyici, boolean dönen bir fonksiyonun neyi kanıtladığını bilmez. Tip yüklemi (type predicate), bu bilgiyi imzada taşır:

interface Olcum {
  kimlik: string;
  deger: number;
  birim: "C" | "Pa" | "%";
}

function olcumMu(deger: unknown): deger is Olcum {
  if (typeof deger !== "object" || deger === null) {
    return false;
  }
  const aday = deger as Record<string, unknown>;
  return (
    typeof aday.kimlik === "string" &&
    typeof aday.deger === "number" &&
    (aday.birim === "C" || aday.birim === "Pa" || aday.birim === "%")
  );
}

function oku(ham: string): string {
  const cozulen: unknown = JSON.parse(ham);
  if (!olcumMu(cozulen)) {
    return "gecersiz kayit";
  }
  return `${cozulen.kimlik}=${cozulen.deger}${cozulen.birim}`;
}

console.log(oku('{"kimlik":"s-01","deger":21.4,"birim":"C"}'));
console.log(oku('{"kimlik":"s-02","deger":"22.1","birim":"C"}'));
console.log(oku('{"kimlik":"s-03","deger":22.1,"birim":"K"}'));

Çıktı:

s-01=21.4C
gecersiz kayit
gecersiz kayit

Dönüş tipi boolean değil deger is Olcum yazılmıştır. Bu, derleyiciye şunu söyler: “bu fonksiyon true döndürdüğünde, argümanının Olcum olduğunu kabul et.” Sonuç, oku içinde cozulen değerinin unknown olmaktan çıkıp Olcum olmasıdır.

Bu, kursun eksenindeki tek noktada toplanır: doğrulama çalışma zamanında yapılır, tip yüklemi bunun kanıt olarak kaydedilmesini sağlar. Model artık dış veriden güvenle kurulabilir.

Yüklemin bir bedeli vardır. Derleyici, gövdenin gerçekten iddia edileni doğruladığını sınamaz:

interface Olcum {
  kimlik: string;
  deger: number;
}

function olcumMu(deger: unknown): deger is Olcum {
  return typeof deger === "object";
}

const aday: unknown = null;
if (olcumMu(aday)) {
  console.log(aday.deger.toFixed(1));
}

Dosya tip denetiminden geçer; çalıştırıldığında:

TypeError: Cannot read properties of null (reading 'deger')

Gövde typeof null === "object" sonucunu true verir ve yanlış bir kanıt üretir. Tip yüklemi, as gibi bir borçtur: doğruluğundan yazan sorumludur. Bu nedenle yüklem gövdeleri kısa tutulur ve sınanır.

Doğrulama Fonksiyonları

Yüklemin ikinci biçimi, sınamayı geçemeyen değerde hata atar:

interface Olcum {
  kimlik: string;
  deger: number;
}

function olcumOldugunuDogrula(deger: unknown): asserts deger is Olcum {
  if (
    typeof deger !== "object" ||
    deger === null ||
    typeof (deger as Record<string, unknown>).kimlik !== "string" ||
    typeof (deger as Record<string, unknown>).deger !== "number"
  ) {
    throw new TypeError("olcum kaydi degil");
  }
}

const cozulen: unknown = JSON.parse('{"kimlik":"s-01","deger":21.4}');
olcumOldugunuDogrula(cozulen);
console.log(cozulen.kimlik, cozulen.deger.toFixed(1));

Çıktı s-01 21.4tür.

asserts deger is Olcum bildirimi, çağrının normal döndüğü durumda tipin daralmasını sağlar. Bir if bloğuna gerek kalmaz; çağrıdan sonraki tüm kodda cozulen bir Olcumdur.

İki biçim arasındaki seçim, geçersiz verinin beklenen bir durum olup olmadığına bağlıdır. Programlama Temelleri kursundaki Tip Dönüşümü dersinde aynı ayrım yapılmıştı: kullanıcı girdisinde geçersizlik olağandır ve yüklemle ele alınır; programın kendi ürettiği verinin geçersizliği istisnaidir ve doğrulama fonksiyonu uygundur.

Bir kısıt vardır: doğrulama fonksiyonunun çağrıldığı ad, açıkça tiplenmiş bir bildirim olmalıdır. Fonksiyon önce tipsiz bir sabite atanır ve o sabit üzerinden çağrılırsa derleyici TS2775 tanısını verir: doğrulama çağrılarında hedefteki her adın açık tip açıklaması taşıması gerekir.

Daraltmanın Kaybolduğu Yer

Denetim akışı çözümlemesi, değerin sınamadan sonra değişmediğini varsayabildiği sürece çalışır. Kapanış içinde bu varsayım geçersizdir:

interface Olcum {
  kimlik: string;
  son?: number;
}

function yaz(kayit: Olcum): void {
  if (kayit.son !== undefined) {
    console.log(kayit.son.toFixed(1));
    [1, 2].forEach(() => {
      console.log(kayit.son.toFixed(1));
    });
  }
}

yaz({ kimlik: "s-01", son: 21.4 });
n6.ts(10,19): error TS18048: 'kayit.son' is possibly 'undefined'.

Sekizinci satır geçerlidir, onuncu satır değildir. Fark, geri çağrının ne zaman çalışacağının bilinmemesidir: forEach senkron çalışır ama derleyici bunu bilemez, ve arada kayit.son alanı silinmiş olabilir. Aynı sorun zamanlayıcılarda ve eşzamansız kodda daha da belirgindir.

Çözüm, daraltılmış değeri yerel bir sabite almaktır:

interface Olcum {
  kimlik: string;
  son?: number;
}

function yaz(kayit: Olcum): void {
  const son = kayit.son;
  if (son !== undefined) {
    console.log(son.toFixed(1));
    [1, 2].forEach(() => {
      console.log(son.toFixed(1));
    });
  }
}

yaz({ kimlik: "s-01", son: 21.4 });

const ile bağlanan bir ad değişemez; derleyici daraltmayı kapanış içine taşıyabilir. Çıktı üç satır 21.4tür.

Özet

  • Daraltma, denetim akışındaki sınamaların tip düzeyine yansımasıdır; typeof, in, instanceof ve eşitlik sınamaları yerleşik koruyuculardır.
  • Doğruluk sınaması 0 ve "" gibi geçerli değerleri de eler; null ve undefined için eşitlik sınaması yazılır.
  • Tip yüklemi (deger is T), ayrı bir fonksiyona taşınmış doğrulamanın sonucunu derleyiciye bildirir; gövdenin doğruluğu sınanmaz, yüklem bir borçtur.
  • Doğrulama fonksiyonu (asserts deger is T), geçersiz değerde hata atar ve normal döndüğünde tipi daraltır.
  • Daraltma kapanış içine taşınmaz; daraltılan değer yerel bir sabite alınarak bu sınır aşılır.

Sonraki Adım

Ölçüm kaydının değişik biçimleri şimdiye kadar elle yazıldı: salt okunur sürüm, alanları seçmeli olan güncelleme sürümü, yalnızca birkaç alanı taşıyan özet sürüm. Her biri asıl tipin bir dönüşümüdür ve tip değiştiğinde birlikte güncellenmeleri gerekir. Sonraki ders bu dönüşümleri tip düzeyinde hesaplayan eşlenmiş tipleri kuracak.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat