Typescript 2020+'da typeguard'lar ile çalışma zamanında bir objeyi türüne veya arayüzüne göre kontrol etme


Benim için çoğu zaman, fetch yanıtının doğrulanması için dinamik kontrol gereklidir. Ve düşünüyorum ki, bu kullanıcı tanımlı typeguard ile herhangi bir türdeki bir nesne için çoklu özellikler ve ek kontroller ile genel bir şekilde yapılabilir mi, böylece şu şekilde kullanılabilir:

Typescript oyun alanı.

İşte örnek bir nesneyle bir örnek, ama ben onsuz bir fonksiyon istiyorum.

// ================= shared exported =================
type Writer = {
  name: string
  age: number
}

type Book = {
  id: number
  name: string
  tags: string[] | null
  writers: Writer[]
}

// function to check object with multiple props general shape, to not do it by hand
function ofType<T>(obj: any): obj is T {
  if (!obj) return false;

   // how to?
   return true // or false 
}

// ================= used and defined in components =================
function isBook(obj: any): obj is Book {
  if (!ofType<Book>(obj)) return false //checking for shape and simple types

  // cheking for specific values and ranges
  if (obj.id < 1) return false 
  if (obj.writers && obj.writers.some(( { age } )=> age < 5 || age > 150)) return false 

  return true
}


const book = {
  id: 1,
  name: 'Avangers',
  tags: ['marvel', 'fun'],
  writers: [ {name: 'Max', age: 25}, {name: 'Max', age: 25}]
}

console.log(isBook(book)) // true or false

Cevap #1

Aslında, TypeScript tür bilgilerini veri yapılarını doğrulamak için kullanılabilecek çalışma zamanı bilgisine çevirmeyi amaçlayan bir dizi modül vardır.

Burada çeşitli çözümleri listelemeye ve karşılaştırmaya çalışacağım. (yaklaşık olarak ne kadar etkili/genel amaçlı olduklarını tahmin ettiğim sırayla; evet, bu biraz özneldir!)

Temel özellikler: (✔️evet, ❌hayır, ⚙️kısmi, ❔bilinmiyor)

ts-base TS temeli: Standart TypeScript tipleri, tip-metadata için temel olarak kullanılır. (tersi yerine)
class Sınıflar: Sadece arayüzler yerine sınıflar için tip-metadata oluşturabilir (şekle dayalı, instanceof değil).
func Fonksiyonlar: Fonksiyonlar için tip-metadata oluşturabilir.
guard Tip-koruyucular: Tip-metadata karşısında çalışma zamanı verilerini doğrulamak için fonksiyonlar içerir. (yani tip-koruyucular)
auto Otomatik kontrol: Dahil edilen tip-koruyucuların çağrılarını otomatik olarak oluşturabilir.

Çözümler

typescript-is

GitHub: 500 NPM: 2.555 (30-09-2020)

Temel özellikler: ts-base: ✔️ class: ❌ func: ❌ guard: ✔️ auto: ⚙️
Not: Otomatik kontrol kısmi olarak işaretlenmiştir, çünkü argüman tiplerinin kontrol edilmesi için sınıf metotlarına (ama bağımsız fonksiyonlara değil) dekoratör ekleyebilirsiniz.

typescript-json-schema (+ bir şema doğrulayıcı, örn. ajv)

GitHub: 1.400 NPM: 51.664 (2020-09-30)

Temel özellikler: ts-base: ✔️ class: ✔️ func: ❌ guard: ❌ auto: ❌
Avantaj: Geçerli json şemaları üretir, bunların ek kullanımları da vardır. (örneğin, diğer dillerde veri doğrulama için kullanılabilir)
Dezavantaj: Üretilen şemaları diske yazmak, çalıştırma sırasında kullanılabilir hale getirmek ve seçtiğiniz JSON şema doğrulayıcısına vermek için biraz manuel çalışma gerektirir.

typescript-rtti

GitHub: 54 NPM: 648 (2022-03-09)

Temel özellikler: ts-base: ✔️ sınıf: ✔️ fonksiyon: ✔️ koruma: ❌ otomatik: ❌
Pro: TypeScript türleri hakkında, tür korumasının ötesinde işlevsellik için kullanılabilir zengin meta veriler sağlar.

tst-yansıt

GitHub: 77 NPM: 79 (2022-03-09)

Temel özellikler: ts-base: ✔️ sınıf: ✔️ fonksiyon: ✔️ koruma: ❌ otomatik: ❌
Pro: TypeScript türleri hakkında, tür korumasının ötesinde işlevsellik için kullanılabilir zengin meta veriler sağlar.

ts-runtime

GitHub: 313 NPM: 96 (2020-09-30)

Temel özellikler: ts-base: ✔️ class: ✔️ func: ✔️ guard: ✔️ auto: ✔️
Eksik: Şu anda yalnızca belirli dosyalara veya fonksiyonlara uygulanamıyor; proje genelinde tip-koruma çağrıları ekliyor. (ama PR’ler muhtemelen memnuniyetle karşılanıyor)
Eksik: Şu notu içeriyor: "Bu paket hala deneysel ve ortaya çıkan kodun üretimde kullanılması amaçlanmamıştır. Bu bir kavram kanıtıdır..."

io-ts (yalnız)

GitHub: 3.600 NPM: 296.577 (2020-09-30)

Temel özellikler: ts-base: ❌ class: ❌ func: ❌ guard: ✔️ auto: ❌
Avantaj: Çalışmak için herhangi bir typescript dönüştürücüye, webpack eklentisine veya CLI komutuna gerek yoktur. (Özel tür tanımı yapısından TS türlerini çıkarsamak için "hileler" kullanır)

io-ts-dönüştürücü (io-ts için eklenti)

GitHub: 16 NPM: 7 (2020-09-30)

Temel özellikler: ts-base: ✔️ class: ❌ func: ❌ guard: ✔️ auto: ❌

ts-otomatik-koruma

GitHub: 134 NPM: 46 (2020-09-30)

Temel özellikler: ts-base: ✔️ sınıf: ❔ fonksiyon: ❌ koruma: ✔️ otomatik: ❌
Dezavantaj: Tip-koruma oluşturulmasını istediğiniz her arayüze özel bir js-doc etiketi eklemeniz gerekir. (zahmetli ve hata yapmaya eğilimli)

yalnızca tür

GitHub: 25 NPM: 101 (2020-09-30)

Temel özellikler: ts-base: ✔️ class: ❔ func: ❌ guard: ✔️ auto: ❌
Eksisi: Genel türler için type-guard’lar oluşturamaz. (buraya bakın)

ts-tipi-kontrol-edildi

GitHub: 13 NPM: 3 (2020-09-30)

Temel özellikler: ts-base: ✔️ class: ❔ func: ❌ guard: ✔️ auto: ❌

Henüz değerlendirilmedi: ts-json-schema-generator, typescript-to-json-schema, gfx/typescript-rtti Dışlandı (halka açık repo yok): typescript-runtime-types

Feragatname

Listelenen çözümlerin hiç birinin yaratıcısı ya da bakımcısı ben değilim. Listeyi, geliştiricilerin çeşitli çözümleri tutarlı bir kriter seti üzerinde karşılaştırmalarına yardımcı olmak için oluşturdum ve GitHub yıldızları ve NPM haftalık indirmeleri gibi faydalı bilgileri ekledim. (Bu değerleri periyodik olarak güncel tutmak için düzenlemeler yapılabilir -- ancak son-güncelleme-zaman etiketlerini değiştirmeyi unutmayın!)

Yeterli itibara sahip olanlar, karşılaştığınız ek çözümleri eklemekten çekinmeyin. (ancak lütfen yeni eklemelerinizdeki metni mevcut olanlarla tutarlı tutmaya çalışın)

Cevap #2

TypeScript'in tür sistemi JavaScript'e derlendiğinde silinir. Bu, yalnızca standart tsc derleyicisini type veya interface tanımlarından çalışma zamanı tür koruyucuları oluşturmak için kullanma çabasının başarılı olmayacağı anlamına gelir; bu tanımlardan çalışma zamanında kullanabileceğiniz hiçbir şey kalmaz. Bu nedenle ofType<T>() uygulanamaz.

Peki sen ne yapabilirsin?


Eğer derleme sisteminizde başka bir derleme adımı kullanmaya istekliyseniz, bu tanımlardan silinmeden önce sizin için tip koruyucuları (type guards) oluşturan bir dönüştürücü yazabilir veya kullanabilirsiniz. Örneğin, typescript-is bunu yapacaktır.


Veya bunun yerine class tanımları kullanabilirsiniz; bu, çalışma zamanında kontrol etmeyi kolaylaştırır (sadece instanceof kullanın) ama zor olan kısım, JSON'u bir sınıf örneğine serileştirmek ve bunu kendiniz manuel olarak yazmadan serileştirme sırasında hataları yakalamaktır. Tüm bunlar, sorununuzu ofType<Book>(someObj) uygulamaktan, myDeserializerFunction(Book, someObj) uygulamaya taşır; burada Book bir sınıf yapıcısıdır.

En azından burada dekoratörleri ve sınıf meta verilerini kullanarak programatik çözümleme için gereken kodu üretebilirsiniz. Bunu kendiniz yazabilir veya json2typescript gibi mevcut bir kütüphaneyi kullanabilirsiniz.


Son olarak, tip koruyucularla başlamaya ve TypeScript'in tip tanımlarınızı bunlardan tahmin etmesini sağlamaya karar verebilirsiniz. Yani, Book tanımlamak ve bundan bir tip koruyucu bookGuard() elde etmeyi ummak yerine, tip koruyucu bookGuard() yazarsınız ve Book'u typeof bookGuard cinsinden tanımlarsınız.

Bu tür gardı, mevcut daha basit tür gardlarını bir araya getirerek oluşturabilirsiniz, böylece bir veri kontrol fonksiyonundan çok bir bildirgesel tür tanımı gibi görünür. Bunu kendiniz yazabilir veya io-ts ya da zod gibi mevcut bir kütüphaneyi kullanabilirsiniz.

Bu yaklaşım için, birinin böyle bir kütüphaneyi nasıl yazabileceğine bakmak öğreticidir. İşte olası bir uygulama:

export type Guard<T> = (x: any) => x is T;
export type Guarded<T extends Guard<any>> = T extends Guard<infer V> ? V : never;
const primitiveGuard = <T>(typeOf: string) => (x: any): x is T => typeof x === typeOf;
export const gString = primitiveGuard<string>("string");
export const gNumber = primitiveGuard<number>("number");
export const gBoolean = primitiveGuard<boolean>("boolean");
export const gNull = (x: any): x is null => x === null;
export const gObject =
    <T extends object>(propGuardObj: { [K in keyof T]: Guard<T[K]> }) =>
        (x: any): x is T => typeof x === "object" && x !== null &&
            (Object.keys(propGuardObj) as Array<keyof T>).
                every(k => (k in x) && propGuardObj[k](x[k]));
export const gArray =
    <T>(elemGuard: Guard<T>) => (x: any): x is Array<T> => Array.isArray(x) &&
        x.every(el => elemGuard(el));
export const gUnion = <T, U>(tGuard: Guard<T>, uGuard: Guard<U>) =>
    (x: any): x is T | U => tGuard(x) || uGuard(x);

Burada birkaç tip bekçisi ve mevcut tip bekçilerini birleştiren fonksiyonları dışa aktarıyoruz. gString(), gNumber(), gBoolean() ve gNull() fonksiyonları sadece tip bekçileri iken, gObject(), gArray() ve gUnion() mevcut tip bekçilerini alıp bunlardan yeni tip bekçileri oluşturur. gObject()'in, tip bekçisi özelliklerle dolu bir nesneyi alıp her özelliğin karşılık gelen bekçiye göre kontrol edildiği yeni bir tip bekçisi oluşturmasını görebilirsiniz. gIntersection() veya gPartial() gibi başka bileşim fonksiyonları ekleyebilirsiniz, ancak buradaki fonksiyonlar örneğiniz için yeterlidir.

Şimdi Book ve Writer tanımlarınız şöyle görünüyor (yukarıdakilerin G isim alanı olarak içe aktarıldığını varsayın):

const _gWriter = G.gObject({
    name: G.gString,
    age: G.gNumber,
});
interface Writer extends G.Guarded<typeof _gWriter> { }
const gWriter: G.Guard<Writer> = _gWriter;

const _gBook = G.gObject({
    id: G.gNumber,
    name: G.gString,
    tags: G.gUnion(G.gArray(G.gString), G.gNull),
    writers: G.gArray(gWriter)
})
interface Book extends G.Guarded<typeof _gBook> { }
const gBook: G.Guard<Book> = _gBook;

Eğer buna dikkatle bakarsanız, bunun örneğinizdeki Writer ve Book tanımlarına benzer olduğunu göreceksiniz. Ama bizim durumumuzda temel nesneler tip bekçileri gWriter ve gBook, ve Writer ve Book türleri onlardan türetilmiştir. Ve sonra var olmayan ofType<Book>() yerine doğrudan gBook kullanabilirsiniz:

const book = JSON.parse('{"id":1,"name":"Avangers","tags":["marvel","fun"],' +
    '"writers":[{"name":"Max","age":25},{"name":"Max","age":25}]}');

if (gBook(book)) {
    console.log(book.name.toUpperCase() + "!"); // AVANGERS!
}

Tamam, umarım yardımcı olur; iyi şanslar!

Koda yönelik oyun alanı bağlantısı

İlgili makaleler