TypeScript'te bir anahtar bloğunun kapsamlı olup olmadığını nasıl kontrol ederim?


97

Bazı kodum var:

enum Color {
    Red,
    Green,
    Blue
}

function getColorName(c: Color): string {
    switch(c) {
        case Color.Red:
            return 'red';
        case Color.Green:
            return 'green';
        // Forgot about Blue
    }

    throw new Error('Did not expect to be here');
}

Color.BlueDavayı halletmeyi unuttum ve bir derleme hatası almayı tercih ederim. TypeScript bunu bir hata olarak işaretleyecek şekilde kodumu nasıl yapılandırabilirim?


1
Yeterince aşağı kaydırırsanız insanlara @Carlos Gines'tan iki satırlık bir çözüm olduğunu bildirmek istedim.
Noumenon

Yanıtlar:


122

Bunu yapmak için never, oluşmaması gereken değerleri temsil eden türü (TypeScript 2.0'da tanıtılan) kullanacağız.

İlk adım bir fonksiyon yazmaktır:

function assertUnreachable(x: never): never {
    throw new Error("Didn't expect to get here");
}

Ardından, defaultdurumda (veya eşdeğeri, anahtarın dışında) kullanın:

function getColorName(c: Color): string {
    switch(c) {
        case Color.Red:
            return 'red';
        case Color.Green:
            return 'green';
    }
    return assertUnreachable(c);
}

Bu noktada bir hata göreceksiniz:

return assertUnreachable(c);
       ~~~~~~~~~~~~~~~~~~~~~
       Type "Color.Blue" is not assignable to type "never"

Hata mesajı, kapsamlı anahtarınıza eklemeyi unuttuğunuz durumları gösterir! Birden fazla değeri bırakırsanız, örn Color.Blue | Color.Yellow.

Kullanıyorsanız , aramanın önünde buna strictNullChecksihtiyacınız olacağını unutmayın (aksi takdirde isteğe bağlıdır).returnassertUnreachable

İstersen biraz daha meraklı olabilirsin. Örneğin, ayrımcılığa tabi bir birleşim kullanıyorsanız, hata ayıklama amacıyla onaylama işlevindeki diskriminant özelliğini kurtarmak faydalı olabilir. Şöyle görünüyor:

// Discriminated union using string literals
interface Dog {
    species: "canine";
    woof: string;
}
interface Cat {
    species: "feline";
    meow: string;
}
interface Fish {
    species: "pisces";
    meow: string;
}
type Pet = Dog | Cat | Fish;

// Externally-visible signature
function throwBadPet(p: never): never;
// Implementation signature
function throwBadPet(p: Pet) {
    throw new Error('Unknown pet kind: ' + p.species);
}

function meetPet(p: Pet) {
    switch(p.species) {
        case "canine":
            console.log("Who's a good boy? " + p.woof);
            break;
        case "feline":
            console.log("Pretty kitty: " + p.meow);
            break;
        default:
            // Argument of type 'Fish' not assignable to 'never'
            throwBadPet(p);
    }
}

Bu güzel bir model çünkü beklediğiniz tüm vakaları ele aldığınızdan emin olmak için derleme zamanı güvenliğini elde ediyorsunuz. Ve eğer gerçekten kapsam dışı bir özellik elde ederseniz (örneğin, bazı JS arayanlar yeni oluşturmuşsa species), faydalı bir hata mesajı atabilirsiniz.


3
İle strictNullChecksetkin, o kadar işlevin dönüş türünü tanımlamak için yeterli değildir stringbir ihtiyaç duymadan, assertUnreachablehiç fonksiyonu?
dbandstra

1
@dbandstra Bunu kesinlikle yapabilirsiniz, ancak genel bir model olarak assertUnreachable daha güvenilirdir. Olmadan bile çalışır strictNullChecksve ayrıca anahtarın dışından tanımsız dönmek istediğiniz koşullar varsa çalışmaya devam eder.
Letharion

Bu, numaralandırma harici bir d.ts dosyasında tanımlandığında işe yaramıyor gibi görünüyor. En azından Microsoft Office JS eklenti API'sinden Office.MailboxEnums.RecipientType numaralandırmasıyla çalışmasını sağlayamıyorum.
Søren Boisen

Yukarıdaki çözüm, orijinal açıklamadaki temel örnek için işe yarıyor, ancak bunun genel olarak uygulanabilir bir model olduğundan emin değilim. Renk numaralandırmasını değiştirin type InDiscriminant = { a: string } | { b: string }ve nasıl devam edilebilir? Basit bir anahtar artık tüm telli değerleri bilmez ve birliğin üye türleri arasında ayırt edilebilecek ortak özellikler yoktur. Bu dava için deyimsel bir çözüm var mı?
cdaringe

noUnusedParameterstsconfig kurulumu, alt çizgi ile önek ekleyerek düzeltebileceğiniz bir "x asla kullanılmaz" hatası function assertUnreachable(_x: never): never { throw new Error("Didn't expect to get here"); }
atacak

33

neverSonuna herhangi bir şey eklemenize veya kullanmanıza gerek yok switch.

Eğer

  • Kişisel switchaçıklamada her durumda döner
  • strictNullChecksDaktilo derleme bayrağını açtınız
  • İşlevinizin belirli bir dönüş türü var
  • Dönüş türü değil undefinedveyavoid

switchHiçbir şeyin iade edilmediği bir durum olacağından, ifadeniz kapsamlı değilse bir hata alırsınız .

Örneğinizden, eğer yaparsanız

function getColorName(c: Color): string {
    switch(c) {
        case Color.Red:
            return 'red';
        case Color.Green:
            return 'green';
        // Forgot about Blue
    }
}

Aşağıdaki derleme hatasını alacaksınız:

İşlev, son dönüş ifadesine sahip değildir ve dönüş türü içermez undefined.


Bu çözüm, bir işlevin tüm olası dönüş değerlerini açıkça belirtmeyi gerektirir. Diğer durumlarda bunları atlayabilir ve derleyicinin tür çıkarımı yapmasına izin verebiliriz.
Aleksei

evet, derleyici şikayet edecek, ancak yine de çalışma zamanında yanlış bir değer alabileceğinizi de göz önünde bulundurmalısınız. Böyle bir durumda anlamlı bir hata mesajı atmayı tercih ederim (örtük olarak tanımsız olarak döndürmek yerine)
TmTron

6
Fikir geri dönmek değilundefined , case ifadesinin eksik dalını yaratmaktır. Bu şekilde çalışma zamanında bir hata almazsınız ve herhangi bir şey atmanıza gerek kalmaz.
Marcelo Lazaroni

3
Kod tam olarak açıklandığı gibi çalışır. Türü görmezden gelirseniz ve cbunun herhangi bir şey olabileceğini düşünürseniz, TypeScript kullanmak için hiçbir neden yoktur. Eğer cboş veya tanımlanmamış olabilir, tip söylemek gerekir ve uygulama bunu da dikkate alacaktı.
Marcelo Lazaroni

1
Bu çözüm bir typcript hatasını tetiklemeye çalışır, ancak hata her zaman açık değildir. defaultAçıklayan neverbir açıklamada olduğu gibi , kullanılmayan bir değişkenin yazıldığı bir durumu kullanmayı tercih ederim "hey bud, if you're getting an error here, you forgot to add a case". YMMV.
Matthias

20

Çözüm

Yaptığım şey bir hata sınıfı tanımlamak:

export class UnreachableCaseError extends Error {
  constructor(val: never) {
    super(`Unreachable case: ${JSON.stringify(val)}`);
  }
}

ve sonra bu hatayı varsayılan durumda atın:

enum Color {
    Red,
    Green,
    Blue
}

function getColorName(c: Color): string {
  switch(c) {
      case Color.Red:
          return 'red, red wine';
      case Color.Green:
          return 'greenday';
      case Color.Blue:
          return "Im blue, daba dee daba";
      default:
          // Argument of type 'c' not assignable to 'never'
          throw new UnreachableCaseError(c);
  }
}

Sanırım okuması Ryan tarafından önerilen fonksiyon yaklaşımından daha kolay çünkü throwcümle, varsayılan sözdizimi vurgulamasına sahip.

İpucu

Ts-şartları kütüphane sınıf vardır UnreachableCaseError bu kullanım örneği için tam

Çalışma zamanı konuları

Daktilo kodunun javascript'e aktarıldığına dikkat edin: Bu nedenle, tüm daktilo tip kontrolleri yalnızca derleme zamanında çalışır ve çalışma zamanında mevcut değildir : yani değişkenin cgerçekten tipte olduğuna dair bir garanti yoktur Color.
Bu, diğer dillerden farklıdır: örneğin Java, türleri çalışma zamanında da kontrol eder ve işlevi yanlış türde bir bağımsız değişkenle çağırmaya çalışırsanız anlamlı bir hata verir - ancak javascript bunu yapmaz.

Bu defaultdurumda anlamlı bir istisna atmanın önemli olmasının nedeni budur : Stackblitz: anlamlı hata atın

Bunu yapmadıysanız, işlev getColorName()örtük olarak geri dönecektir undefined(beklenmedik bir argümanla çağrıldığında): Stackblitz: herhangi birini döndür

Yukarıdaki örneklerde any, sorunu açıklamak için doğrudan bir tür değişkeni kullandık . Bu umarız gerçek dünya projelerinde olmaz - ancak çalışma zamanında yanlış türde bir değişken elde etmenin başka birçok yolu vardır.
İşte daha önce gördüğüm (ve bu hatalardan bazılarını kendim yaptım):

  • açısal formlar kullanma - bunlar tür güvenli değildir: tüm form alanı değerleri any
    ng formları tipindedir Stackblitz örneği
  • örtük herhangi birine izin verilir
  • harici bir değer kullanılır ve doğrulanmaz (örneğin, sunucudan gelen http yanıtı yalnızca bir arayüze aktarılır)
  • yerel depolamadan uygulamanın eski bir sürümünün yazdığı bir değeri okuruz (bu değerler değişti, bu nedenle yeni mantık eski değeri anlamıyor)
  • tür açısından güvenli olmayan (veya yalnızca bir hata içeren) bazı 3. taraf kitaplıklar kullanıyoruz

Bu yüzden tembel olmayın ve bu ek varsayılan durumu yazın - size birçok baş ağrısını azaltabilir ...


Şu anda aşağıdaki instanceofalt sınıflar için kontrol kullanamayacağınızı unutmayın Error: # 13965 numaralı sorun , aynı zamanda bir geçici
çözümden

Kontrol ettim ve şimdi düzgün çalışıyor. TypeScript 3.5.1.
Aleksei

1
Ooooh, bu çok güzel ve aynı zamanda düz JS ile yazıyor olsaydım kodu neredeyse tam olarak nasıl yazardım.
Pete

Derleyici bunu nasıl bulur?
jocull

@jocull ne istediğinden emin değilim. Belki Ayrımcılığa Uğramış Sendikalar hakkında bir şeyler okumak istersiniz ?
TmTron

18

Ryan'ın cevabının üstüne inşa ederek, burada fazladan bir işleve ihtiyaç olmadığını keşfettim . Doğrudan yapabiliriz:

function getColorName(c: Color): string {
  switch (c) {
    case Color.Red:
      return "red";
    case Color.Green:
      return "green";
    // Forgot about Blue
    default:
      const _exhaustiveCheck: never = c;
      throw new Error("How did we get here?");
  }
}

Sen eylem görebilirsiniz burada TS Oyun alanı


2
Ben de bir işlev oluşturmak yerine değişken kullanmayı tercih ederim (her ikisi de paket zamanında soyulabilir). Her halükarda, linteriniz kullanılmayan bir değişkenden şikayetçi olabilir ve bunun üzerine // eslint-disable-next-line @typescript-eslint/no-unused-vars
Matthias

2
Veya değişkeni throw new Error(`Unexpected: ${_exhaustiveCheck}`);
attığınız hatada

1
İyi fikir. Ayrıca anonim bir işlev de tanımlayabilirsiniz:default: ((x: never) => { throw new Error(c + " was unhandled."); })(c);
Brady Holt

@Nooodles çözümünün SonarQube kullanılmayan değişkenler kontrolünü karşıladığını doğrulayabilirim. Teşekkürler. Bunun ne kadar kısa olduğunu seviyorum.
Noumenon

Yukarıdaki koddan bazı ilgili (bunların oldukça sık olabileceğini düşünüyorum) hataları şu şekilde ele aldım:default: { const pleaseBeExhaustive: never = c; throw new Error(`Use all Color - ${pleaseBeExhaustive as string}`); }
Jonny


5

Ryan'ın cevabında hoş bir değişiklik olarak never, hata mesajını daha kullanıcı dostu hale getirmek için rastgele bir dizeyle değiştirebilirsiniz .

function assertUnreachable(x: 'error: Did you forget to handle this type?'): never {
    throw new Error("Didn't expect to get here");
}

Şimdi, şunları elde edersiniz:

return assertUnreachable(c);
       ~~~~~~~~~~~~~~~~~~~~~
       Type "Color.Blue" is not assignable to type "error: Did you forget to handle this type?"

Bu işe yarar çünkü neverrastgele bir dizge dahil herhangi bir şeye atanabilir.


3

Ryan ve Carlos'un yanıtlarına dayanarak , ayrı bir adlandırılmış işlev oluşturmak zorunda kalmamak için anonim bir yöntem kullanabilirsiniz:

function getColorName(c: Color): string {
  switch (c) {
    case Color.Red:
      return "red";
    case Color.Green:
      return "green";
    // Forgot about Blue
    default:
      ((x: never) => {
        throw new Error(`${x} was unhandled!`);
      })(c);
  }
}

Anahtarınız kapsamlı değilse, bir derleme zamanı hatası alırsınız .


2

Typescript veya linter uyarılarından kaçınmak için:

    default:
        ((_: never): void => {})(c);

bağlamda:

function getColorName(c: Color): string {
    switch(c) {
        case Color.Red:
            return 'red';
        case Color.Green:
            return 'green';
        default:
            ((_: never): void => {})(c);
    }
}

Bu çözüm ile diğerleri arasındaki fark şudur:

  • referans alınmayan adlandırılmış değişken yok
  • Typescript kodun neveryine de çalıştırılmasını zorlayacağından bir istisna atmaz

Kodun ne işe
yaradığını

1

Gerçekten basit durumlarda, enum değerine göre bir dizi döndürmeniz gerektiğinde, anahtar kullanmak yerine sonuçlar sözlüğünü saklamak için bir sabit kullanmak daha kolaydır (IMHO). Örneğin:

enum Color {
    Red,
    Green,
    Blue
}

function getColorName(c: Color): string {
  const colorNames: Record<Color, string> = {
    [Color.Red]: `I'm red`,
    [Color.Green]: `I'm green`,
    [Color.Blue]: `I'm blue, dabudi dabudai`,   
  }

  return colorNames[c] || ''
}

Yani burada her enum değerini sabit olarak belirtmeniz gerekecek, aksi takdirde örneğin Blue eksikse gibi bir hata alırsınız:

TS2741: 'Mavi' özelliği '{[Color.Red] tipinde eksik: string; [Color.Green]: string; ' ancak 'Kayıt' türünde gereklidir.

Ancak çoğu zaman durum böyle değildir ve o zaman tıpkı Ryan Cavanaugh'un önerdiği gibi bir hata atmak gerçekten daha iyidir.

Ayrıca bunun işe yaramayacağını öğrendiğimde biraz üzüldüm:

function getColorName(c: Color): string {
    switch(c) {
        case Color.Red:
            return 'red';
        case Color.Green:
            return 'green';
    }
    return '' as never // I had some hope that it rises a type error, but it doesn't :)
}

-1

Bir switchifade kullanmak yerine özel bir işlev oluşturun .

export function exhaustSwitch<T extends string, TRet>(
  value: T,
  map: { [x in T]: () => TRet }
): TRet {
  return map[value]();
}

Örnek kullanım

type MyEnum = 'a' | 'b' | 'c';

const v = 'a' as MyEnum;

exhaustSwitch(v, {
  a: () => 1,
  b: () => 1,
  c: () => 1,
});

Daha sonra eklerseniz diçin MyEnum, bir hata alırsınızProperty 'd' is missing in type ...


Bu yalnızca, enum değerleriniz dize ise işe yarar, çünkü nesne parametresinde anahtar olarak kodlanmaları gerekir map.
Will Madden
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.