Type[keyof Type] fonksiyon parametresi için Typescript tür koruyucu


Kafa karıştırıcı başlık için özür dilerim.

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#keyof-and-lookup-types bağlantısındaki setProperty örneğine benzer bir arama türü kullanmaya çalışıyorum

Fonksiyon çağrılırken arama türü doğru şekilde kontrol ediliyor ancak fonksiyon içinde kullanılamıyor. Bunu bir tür koruyucu ile aşmayı denedim, ancak bu işe yaramıyor gibi görünüyor.

Örnek :

interface Entity {
  name: string;
  age: number;
}

function handleProperty<K extends keyof Entity>(e: Entity, k: K, v: Entity[K]): void {
  if (k === 'age') {
    //console.log(v + 2); // Error, v is not asserted as number
    console.log((v as number) + 2); // Good
  }
  console.log(v);
}

let x: Entity = {name: 'foo', age: 10 }

//handleProperty(x, 'name', 10); // Error
handleProperty(x, 'name', 'bar'); // Good
// handleProperty(x, 'age', 'bar'); // Error
handleProperty(x, 'age', 20); // Good

TS Oyun Alanı

Typescript'in bunu, (v as number) gibi bir tür doğrulamasını sert bir şekilde kodlamadan çözmesini sağlayacak bir yol var mı? Kodun o noktasında, derleyici v'nin bir sayı olduğunu çıkarabilmelidir.

Cevap #1

İlk problem, derleyicinin handleProperty() fonksiyonunun içinde k değerini kontrol ederek K tip parametresini daraltamamasıdır. (Bkz. microsoft/TypeScript#24085.) Derleyici bunu denemeye bile çalışmaz. Teknik olarak, derleyicinin bunu yapmaması doğrudur, çünkü K extends "name" | "age" ifadesi, K'nin ya "name" ya da "age" olduğu anlamına gelmez. O, tam birleşim "name" | "age" olabilir; bu durumda, k'yi kontrol etmenin K ve dolayısıyla T[K] üzerinde bir etkisi olduğunu varsayamazsınız:

handleProperty(x, Math.random() < 0.5 ? "name" : "age", "bar"); // accepted!

Burada görebileceğiniz gibi k parametresi "name" | "age" türündedir ve bu nedenle K da bu şekilde çıkarılır. Dolayısıyla v parametresinin türü string | number olabilir. Bu yüzden implication içindeki hata doğrudur: k "age" olabilir ve v hâlâ bir string olabilir. Bu, fonksiyonunuzun amacını tamamen bozar ve kesinlikle hedeflediğiniz kullanım durumu değildir, ancak derleyicinin endişelendiği bir olasılıktır.

Gerçekte söylemek istediğiniz şey, ya K extends "name" veya K extends "age", ya da K extends_one_of ("name", "age") gibi bir şeydir (bakınız microsoft/TypeScript#27808), ancak şu anda bunu temsil etmenin bir yolu yok. Bu yüzden genericler gerçekten üzerinde çalışmaya çalıştığınız kontrolü size vermez.

Elbette, birinin handleProperty() metodunu tam birleşim tipi ile çağırmasından endişe etmeyebilirdiniz, ancak uygulamanın içinde v as number gibi bir tür dönüşümüne ihtiyacınız olacak.


Eğer çağrıcıları gerçekten amaçlanan kullanım durumlarıyla sınırlamak istiyorsanız, jeneriklerin yerine dinlenme demetlerinin birleşimini kullanabilirsiniz:

type KV = { [K in keyof Entity]: [k: K, v: Entity[K]] }[keyof Entity]
// type KV = [k: "name", v: string] | [k: "age", v: number];

function handleProperty(e: Entity, ...[k, v]: KV): void {
  // impl
}

handleProperty(x, 'name', 10); // Error
handleProperty(x, 'name', 'bar'); // Good
handleProperty(x, 'age', 'bar'); // Error
handleProperty(x, 'age', 20); // Good
handleProperty(x, Math.random() < 0.5 ? "name" : "age", "bar"); // Error

Görebilirsiniz ki KV tipi, Entity'yi öyle bir tipe eşleyerek oluşturulan ve ardından bu türlerin birleşimini hemen kontrol ederek oluşturulan demetlerin (tuple) birleşimi ve handleProperty() bunun son iki argümanı olarak bunu kabul eder.

Harika, değil mi? Ne yazık ki bu, uygulama içindeki problemi çözmüyor:

function handleProperty(e: Entity, ...[k, v]: KV): void {
  if (k === 'age') {
    console.log(v + 2); // still error!
  }
  console.log(v);
}

Bu, benim korelasyonlu birleşik tipler olarak adlandırdığım şeyi desteklememenin bir sonucudur (bkz. microsoft/TypeScript#30581). Derleyici, ayrıştırılmış k değişkeninin türünü "name" | "age" ve ayrıştırılmış v değişkeninin türünü string | number olarak görüyor. Bu türler doğru, ancak tam hikaye bu değil. Kalan argümanı ayırarak, derleyici ilk öğenin türünün ikinci öğenin türü ile korelasyonlu olduğunu unuttu.


Yani, ondan kaçınmak için, rest argümanını dağıtmayabilir veya en azından ilk öğesini kontrol edene kadar dağıtmayabilirsiniz. Örneğin:

function handleProperty(e: Entity, ...kv: KV): void {
  if (kv[0] === 'age') {
    console.log(kv[1] + 2) // no error, finally!
    // if you want k and v separate
    const [k, v] = kv;
    console.log(v + 2) // also no error
  }
  console.log(kv[1]);
}

Burada geri kalan demeti tek bir dizi değeri kv olarak bırakıyoruz. Derleyici bunu ayrıştırılmış bir birlik (discriminated union) olarak görüyor ve kv[0]'ı (önceden k olanı) kontrol ettiğinizde, derleyici nihayetinde kv'nin türünü sizin için daraltacak ve böylece kv[1] de daraltılmış olacak. kv[0] ve kv[1] kullanmak çirkin, ve kv[0] kontrolünden sonra parçalama yaparak bunu kısmen hafifletebilirsiniz, yine de bu hâlâ ideal değil.


İşte karşınızda, handleProperty() fonksiyonunun tamamen tip güvenli (veya en azından tip güvenliğe daha yakın) bir uygulaması. Buna değer mi? Muhtemelen hayır. Pratikte, derleyici uyarılarını susturmak için tip doğrulaması ile birlikte deyimsel JavaScript yazmak genellikle daha iyi oluyor, tıpkı ilk başta yaptığınız gibi.

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