1 puan yazan GN⁺ 4 시간 전 | 1 yorum | WhatsApp'ta paylaş
  • TypeScript kodunda if (user.email) gibi kontroller dağınık halde bulunursa, doğrulanmış olan gerçek tipte kalmadığı için çağrı yığınının ilerleyen kısımlarında aynı koşuldan sürekli şüphe edilir
  • Parser, ham girdiyi alıp daha dar bir tip ya da hata bilgisi döndürür; EmailAddress gibi doğrulanmış bir gerçeği programın geri kalanının güvenle kullanmasını sağlar
  • Yapısal tip sistemi kullanan TypeScript’te string ile Email doğal olarak ayrışmadığından, unique symbol tabanlı branded type ve sınırlı as assertion kullanarak nominal sınırlar taklit edilir
  • Parsed<T> gibi discriminated union’lar başarı ve başarısızlığı tip imzasında görünür kılar; ancak özel bir match ifadesi olmadığı için never kullanarak exhaustive check’i elle yazmak gerekir
  • Zod, io-ts, valibot şemadan parser ve TypeScript tipini birlikte üretebilir; fakat dış girdiyi domain tipi olarak görmeden önce her sınırda parse etme disiplini hâlâ geliştiriciye kalır

Doğrulama bilgiyi atar, parse etme tipte bırakır

  • Alexis King’in Parse, don’t validate ilkesi, doğrulayıcı ile parser arasındaki farkı merkeze alır
    • Doğrulayıcı “bu değer uygun” diye karar verdikten sonra akışı boolean ya da exception ile devreder
    • Parser ham girdiyi alıp daha kesin bir tip üretir ya da başarısızlık nedenini döndürür
  • User.email: string, User.age: number gibi tipler geniş kaldığında, isValidUser(user): boolean geçse bile TypeScript bu gerçeği hatırlamaz
  • Sonrasında emailService.send(user.email, ...) gibi kodlarda user.email hâlâ boş string, "hello", "definitely not an email" gibi genel bir string’dir
  • Aynı koşulu birçok yerde yeniden kontrol eden akış, King’in sözünü ettiği shotgun parsing’e yakındır

Tipin kendisinin kanıt olduğu API

  • İstenen biçim, sendWelcome(user: ValidUser) gibi yalnızca parse edilmiş değerleri kabul eden bir fonksiyon imzasıdır
  • Bu yapıda sendWelcome çağrılmadan önce mutlaka parser’dan geçilmelidir; fonksiyon içinde ayrıca yeniden doğrulama ya da savunmacı if gerekmez
  • Elm’de opaque type ve smart constructor ile bu kolayca çözülebilir; TypeScript’te ise aynı etki için daha fazla mekanizma gerekir

Branded type ile nominal sınırlar oluşturmak

  • TypeScript yapısal tip sistemi kullanır; bu nedenle aynı shape’e sahip tipler aynı tip kabul edilir
    • string bir string’dir; Haskell’deki newtype gibi gerçekten farklı bir tip oluşturan bir özellik yoktur
  • Toplulukta kullanılan dolaylı çözüm branding ya da etiketlemedir
    • Basit yöntem, { readonly __brand: "Email" } gibi string literal bir phantom field kullanmaktır
    • Daha güçlü yöntem, modül dışına export edilmeyen bir unique symbol’ü brand anahtarı olarak kullanmaktır
  • Örnek tipler type Email = string & { readonly [EmailBrand]: true }, type Age = number & { readonly [AgeBrand]: true } biçimindedir
  • Brand alanı runtime’da var olmayan tip düzeyinde bir işaretçidir; Email ile string’in compile time’da farklı ele alınmasını sağlar
  • Brand yalnızca tek yönde çalışır
    • Email, string’e atanabilir
    • Sıradan bir string, doğrudan Email olarak gelemez

Parser, yalnızca güven sınırında assertion’a izin verir

  • parseEmail(raw: string): Parsed<Email>, string içinde @ yoksa başarısızlık döndürür; geçerse raw as Email ile branded type üretir
  • as Email assertion’ı, parser bir güven sınırı olduğu için izin verilen bir istisnadır
    • Kod tabanının başka bir yerinde string’i Email olarak assert etmek tasarımı bozar
    • Parser ayrı bir modüle konup brand assertion’ı bunun dışında görülürse bug kabul edilebilir
  • Örnekteki Parsed<T> biçimi { kind: "ok"; value: T } | { kind: "err"; error: ParseError } şeklindedir
    • Başarısızlık exception içinde saklanmaz, tip imzasında görünür
    • kind: "ok" | "err" gibi string discriminant kullanıldığında, ileride varyant eklendiğinde type narrowing daha dürüst çalışır
  • parseEmail örneği bilerek incedir; gerçek bir e-posta parser’ı trim, lowercase, domain doğrulaması gibi daha fazla işlemi ele almalıdır

Ham girdi ile güvenilen domain tiplerini ayırmak

  • UnvalidatedUser ile ValidUser ayrılırsa ağdan ya da dış girdiden gelen değerler ile domain içinde güvenilebilecek değerler net biçimde ayrılır
    • UnvalidatedUser, id, email, age alanlarını unknown olarak tutar
    • ValidUser, UserId, Email, Age gibi branded type’lar kullanır
  • UserId de brand edilirse UserId gereken yere OrderId gibi başka bir ID’nin yanlışlıkla verilmesi engellenebilir
  • parseUser(raw: unknown): Parsed<ValidUser> ham girdiyi aşamalı olarak daraltır
    • Girdinin object olup olmadığını kontrol eder
    • id, email, age alanlarının varlığını kontrol eder
    • email’in string olup olmadığını kontrol eder
    • parseUserId, parseEmail, parseAge fonksiyonlarını ayrı ayrı çağırır ve başarısızlıkta hemen döner
    • Hepsi başarılı olursa ValidUser döndürür
  • Bu yaklaşım F# ya da Elm’e göre daha uzun sözdizimlidir; ancak sendWelcome(user: ValidUser) gerçekten güvenli hâle gelir

TypeScript’in pürüz çıkardığı noktalar

  • İlk sürtünme, parser içindeki as Email assertion’ıdır
    • Gerçek nominal tipli dillerde smart constructor yalan söylemeden yeni tipi döndürebilir
    • TypeScript’in brand’leri hayalî tip işaretçileri olduğundan parser’ın assertion’a geçmesi gerekir
  • İkinci sürtünme exhaustive check’tir
    • TypeScript’in discriminated union’ları bu stilde güçlüdür; fakat özel bir match ifadesi yoktur
    • switch içindeki default bölümünde const _exhaustive: never = result gibi pattern’leri elle kullanmak gerekir
    • Parsed’a üçüncü bir varyant eklenirse never ataması başarısız olur ve compiler konumu bildirir
  • satisfies, cast’e göre daha nazik bir escape hatch olarak kullanılabilir
    • const x = { ... } satisfies Config, tipi kontrol ederken literal type’ları gereksiz yere genişletmez
  • JSON.parse any döndürdüğü için hemen unknown olarak anotasyonlamak daha güvenlidir
    • const raw: unknown = JSON.parse(input) biçiminde alınıp, sonrasında parser bunun domain tipi olup olmadığına karar verir
    • JSON.parse bir doğrulayıcı değil, byte’ları JS değerine dönüştüren bir deserialization aşamasıdır

Zod gibi kütüphanelerin azalttığı tekrar

  • Zod, io-ts, valibot, elle yazılmış parser’dan daha kullanışlı biçimde aynı pattern’i sunar
  • Zod örneği, tek bir şemadan parser ve TypeScript tipini birlikte üretir
    • z.object({ id: z.number().int(), email: z.string().email().brand<"Email">(), age: z.number().int().min(0).max(150).brand<"Age">() })
    • Tip, z.infer<typeof ValidUserSchema> ile elde edilir
    • ValidUserSchema.safeParse(rawInput) başarı durumunda data, başarısızlıkta error döndürür
  • Zod’un .brand() özelliği de elle yapılmış symbol brand gibi tip düzeyinde bir özelliktir; runtime davranışı yoktur
  • Kütüphane, parser ile tipi aynı tanımda birleştirerek sınırları korumayı kolaylaştırır; fakat bunun tüm dış sınırlarda kullanılması gerektiği disiplinini geliştirici yerine zorunlu kılmaz
  • Ağdan gelen User, parse edilene kadar domain User değildir; tip assertion ile hata mesajını baypas etme cazibesinden kaçınmak gerekir

Kanıtı hafızada değil, tipte taşımak

  • Küçük ilke şudur: “Tip sisteminin kanıtı taşımasını sağla, insan hafızasına bırakma”
  • Bir koşulu kontrol edip sonucunu tipe encode etmezseniz, sonraki kod bu doğrulamanın çoktan bittiğini kolayca varsayar
  • TypeScript’te bu ilke üç araca dayanarak uygulanır
    • Nominal kimliği taklit eden branded type’lar
    • Başarı ve başarısızlığı görünür kılan discriminated union’lar
    • Dış girdinin unknown olması ile güvenilen domain tipleri arasındaki sıkı sınır
  • Tüm kodu parsing pipeline’a dönüştürmek her zaman uygun değildir; ancak aynı savunmacı if birçok dosyada tekrarlanıyorsa bu, doğrulanması gereken bilginin tipe taşınamadığının işaretidir

1 yorum

 
GN⁺ 4 시간 전
Lobste.rs yorumları
  • JavaScript/TypeScript’in istediği kod stiliyle teknik ve ergonomik olarak çakışıyorsa, JS’ye derlenen dillerden birini kullanmak yeterli olmaz mı diye düşünüyorum.
    Haskell, Elm, F# anılıyor; ayrıca PureScript, js_of_ocaml, Reason, LunarML gibi yazarın daha çok kullanmak isteyeceği çizgide pek çok dil de var. Yazar Why TypeScript Won’t Save You başlıklı bir yazı bile yazıp tercih ettiği dillerle daha fazla karşılaştırma yapmış ve https://learnelm.dev sitesini de işletiyor.
    Ya da belki amaç zaten karşılaştırmanın kendisidir; TypeScript’in çoğu durumda yeterli olmadığını gösterip başka toolchain’lerin ya da fikirlerin benimsenmesini teşvik etmek istiyor olabilir

    • Mevcut kod tabanı, ekibin belirli bir dildeki yetkinliği veya şirket yönergeleri, daha az destek/araç/topluluk ölçeği gibi kısıtlar var.
      Çoğu kişinin başka bir dil seçme özgürlüğü ya da zamanı yok
    • Genelde büyük bir TypeScript kod tabanı olduğu ya da başka dillerde bulunmayan TypeScript kütüphaneleri kullanıldığı için olabilir
  • İşte branded type’ları çok seviyorum ama yalnızca branded sayılarla indekslenebilen bir Array ya da TypedArray oluşturamamak gerçekten can sıkıcı.
    TypedArray branded sayıları saklayamıyor; daha doğrusu onları okuyup çıkarmak bile mümkün değil. IndexArray veya IndexTypedArray gibi ayrı bir tip kümesi gerekecek olsa bile, böyle bir özelliğin mutlaka olmasını isterdim

    • Ben de branded type’ları seviyorum ama konuştuğumda herkes harcanan çabaya kıyasla pek değmediğini düşünüyor.
      Oldukça karmaşık bir veritabanı şemasında tüm ID’ler için branded type kullanırsanız, mantıksız join’ler ya da koşullar oluşturduğunuzda TypeScript bunu yakalıyor. Fonksiyon imzaları da daha netleşiyor ve çeşitli hatalar yapmak zorlaşıyor
    • Yeterince güçlü biçimde yalan söylemeye razıysanız, yalnızca branded sayılarla indekslenebilen bir Array’i oluşturabilirsiniz.
      İsterseniz TypedArray değerleri için de aynı şekilde yapmak mümkün
    • İş yerinde “smart enum” ve özel dizi tipleri kullanarak TArray<Foo, MyEnum> gibi yazabiliyoruz. Ancak bu C++ için geçerli.
      Zig’in std kütüphanesinde comptime ile uygulanmış EnumArray var. Yoğun enum’ları ya da seyrek enum’ları indeksleme için kullanma, derleme zamanında doğru indeksleyiciyi hesaplama gibi daha geniş özellikler de sunuyor.
      Bu tür hassas tipleme giderek daha çok hoşuma gidiyor. Kod tabanına mantık hatalarının girmesini baştan büyük ölçüde engelliyor