簡體   English   中英

在 TypeScript 中管理可區分聯合

[英]Manage discriminated unions in TypeScript

枚舉不起作用

TypeScript 枚舉接近我所需要的,但它們並沒有完全到達那里。 我需要一個在編譯時已知在運行時靜態的項目集,它們的作用類似於枚舉,但它們是類的實例,可以添加屬性或方法等功能。

出於此問題的目的,假設 TypeScript 4.4.3。

例子

例如,假設一家小型便利店有三個過道。 每個過道都有一個id和一個name

// Aisle objects
name: 'food', aisleId: '4'
name: 'housewares', aisldId: '7'
name: 'camping', aisleId: '9'

當一個過道的name或過道aisleId包含在一個變量中時, Aisles類上的一些靜態輔助方法是有意義的:

static getByName(name: string): Aisle {
  // return the right Aisle, or return undefined, or maybe throw
}

static getById(aisleId: string): Aisle {
  // same, but for aisleId
}

(可能有description屬性和其他屬性,但不會通過描述對 Aisle 對象進行任何查找。)

如果某些代碼想要使用特定的Aisle ,那么編寫Aisles.FoodAisles['7']會很好。 雖然上述getBy方法可用於,這不是美妙必須使用Aisles.getById('food')時, 'food' ,在編譯時是已知的。 如果這些必須在不同的枚舉中,例如AisleNames.FoodAisleIds['7'] ,那AisleIds['7']

目標

實現所有或大部分這些目標會很棒:

  • 僅適用於傳入的Aisle實例的代碼應該能夠依賴環境聲明,而無需導入任何內容: function cleanAisle(aisle: Aisle) {}不應導入任何內容,因為它不會創建任何實例。
  • 所有過道的類似枚舉的集合可以在某處公開:也許是AisleList Aisles AislesEnum
  • 環境類型聲明規范地驅動整個代碼庫的類型安全。 需要兩個潛在沖突的類型/類信息源,一個公開枚舉或類似枚舉的對象的.ts文件,以及.d.ts枚舉的環境.d.ts文件中的一個單獨的文件,兩者都不是.d.ts作為單一的規范來源。
  • 如果存在任何不一致之處,則應該在某處發生類型錯誤:如果代碼的任何位添加或刪除了新通道,或者命名錯誤或拼寫錯誤,或者在不應該存在的地方存在任何重復。
  • 該方案具有最少的代碼重復量。 例如,命名屬性並不好,而是將與字符串相同的名稱傳遞給構造函數,以便類實例可用於返回其自己的名稱,例如, readonly food = new Aisle('food', '4')重復'food' 這可能會導致屬性和實例之間不匹配。 理想的情況是new Aisle<'food'>()然后驅動實際name屬性或使用名稱作為代碼符號,無處不在,源自環境類型聲明或類的正確實例。 (有一個 TypeScript 插件可以在編譯時實現nameof可能會有所幫助,但我還沒有做到這一點。)

我的嘗試

在我嘗試實現這些目標的過程中,出現了幾個問題。 (您可以在 TypeScript Playground 中看到我的一些面條,但並不是我嘗試的所有內容都在那里。)

我嘗試向Aisle添加泛型類型參數,例如Aisle<'food'> ,但是在將它們放入集合中時,如何為它們使用接口? 例如,給定interface Aisle<T extends AisleName> {}和具體類ConcreteAisle<T extends AisleName> implements Aisle<T> {} ,不能將ConcreteAisle<T>實例放入Aisle[]因為Aisle想要一個泛型type 參數,但沒有單一的泛型類型來給出它。 工會是否會在那里工作,例如Aisle<AisleName>其中type AisleName = 'food' | 'housewares' | 'camping' type AisleName = 'food' | 'housewares' | 'camping' type AisleName = 'food' | 'housewares' | 'camping' 然后唯一性/完整性問題浮出水面。 Set<Aisle>不能保證唯一性。 這就是“TypeScript 中的枚舉不是很好”的用武之地。

enum Aisle as const可能會有所幫助,只要代碼在tsconfig.json不需要"isolatedModules": true 所以這不是一個選擇。

方法getAisleById()getAisleByName()也有一些問題。 使用一Set Aisle對象,並對它們執行.map()以生成Map<AisleName, Aisle>aisleIdMap遇到了問題,即使類型聯合用於傳入的值, Map<K, V>#get方法返回類型V | undefined V | undefined ,需要某種解決方法來誘使 TypeScript 相信只要值在聯合類型中就知道結果不是undefined 我知道如何使用返回var is Type作為返回值的函數,或者asserts var is Type ,但寧願不將它們暴露給這些對象的使用者——這都應該是AislesAisle枚舉的實現的內部構造,如果可能的話。

如有必要,我可以為每個Aisle編寫一個單獨的具體類。 這很好,並且可以讓我在每個中對aisleNameaisleId進行硬編碼, aisleName aisleId這些值傳遞給構造函數。 但是“如何在一個作為可區分聯合的集合中使用這些”問題變得更糟。

有沒有人有任何想法?

為了能夠區分類型的聯合,在具有文字值(布爾值、字符串、數字或唯一符號)的類型之間必須至少有一個公共屬性。 通過向Aisle添加T extends ...屬性,您走上了正確的道路。

這是一種可能的方法,它從您的Aisle類中提取idname參數以獲得最大的編譯時推理能力:

// `id` and `name` will both be pulled out as constants here, allowing you to get
// their exact values later on.
class Aisle<Id extends number, N extends string> {
  constructor(readonly id: Id, readonly name: N) { }
}

// Here's an "enum" of statically known Aisles
const Aisles = {
  1: new Aisle(1, 'baking'),
  2: new Aisle(2, 'garden'),
  3: new Aisle(3, 'spices'),
  4: new Aisle(4, 'food'),
  5: new Aisle(5, 'toys'),
  6: new Aisle(6, 'cleaning'),
  7: new Aisle(7, 'housewares'),
  8: new Aisle(8, 'travel'),
  9: new Aisle(9, 'camping'),
} as const; // Don't forget this

// And here's a type that makes referencing specific Aisles easier
type Aisles = typeof Aisles;

// Like so
type AisleOneOrTwo = Aisles[1] | Aisles[2];

// Accessing a particular Aisle
const a0 = Aisles[1] // Aisle<1, "baking">

// Fields are resolved to their literal values here.
a0.id // 1
a0.name // "baking"

// Here's a full union type of your Aisles.
type AislesUnion = Aisles[keyof Aisles];

這里還有一個到操場鏈接

正如您所注意到的, TypeScript中的enum實際上只是原始字符串或數字。 它們的行為不像Java中的enum后者只是引擎蓋下的類實例。 如果您想要類似枚舉的類實例,則必須自己構建它們。

一種方法可能是不出口的基礎類(這樣新情況下,你不控制不到處露面),並確保他們是強類型足以使產生的枚舉可以表現為一個可識別聯合由具有字面- 值的判別屬性。 看起來您有兩個這樣的屬性, nameaisleId ,所以讓我們在這兩個中使這個底層Aisle通用

class Aisle<N extends string, I extends string> {
    constructor(public name: N, public aisleId: I, public description: string) { }
    someMethod() {
        console.log("Hi, I'm " + this.name + " and my Id is " + 
          this.aisleId + " and my description is " + this.description);
    }
}

由於我們希望導出的Aisles枚舉對象具有這些nameaisleId值中的每一個的鍵,我們可以編寫一個輔助函數,該函數返回一個具有這兩個鍵的對象,其中每個值都是相關的類實例:

function make<N extends string, I extends string>(name: N, aisleId: I, description: string) {
    const aisle = new Aisle(name, aisleId, description);
    return { [name]: aisle, [aisleId]: aisle } as Record<N | I, typeof aisle>;
}

請注意,返回類型必須聲明Record<N | I, typeof aisle> Record<N | I, typeof aisle>因為編譯器不幸地將計算屬性鍵類型一直擴展到string 請參閱microsoft/TypeScript#13948

現在我們可以使用對象傳播來構建我們導出的Aisles枚舉對象:

export const Aisles = {
    ...make("food", "4", 'All things edible'),
    ...make("housewares", "7", 'Make your home zing'),
    ...make("camping", "9", 'Get into the outdoors!')
} as const;

您可以檢查Aisles以檢查它是否包含您關心的所有鍵和值。

/* const Aisles: {
    readonly camping: Aisle<"camping", "9">;
    readonly 9: Aisle<"camping", "9">;
    readonly housewares: Aisle<"housewares", "7">;
    readonly 7: Aisle<"housewares", "7">;
    readonly food: Aisle<...>;
    readonly 4: Aisle<...>;
} */

如果您希望Aisles也是與Aisles對象的值相對應的類型,( enum會自動執行此操作),您可以這樣做:

export type Aisles = typeof Aisles[keyof typeof Aisles];

如果您想將更多類型附加到Aisles您可以通過namespace 例如,相關鍵的並集對應的NameId類型:

namespace Aisles {
    export type Name = Aisles extends infer A ? A extends Aisle<infer N, any> ? N : never : never;
    export type Id = Aisles extends infer A ? A extends Aisle<any, infer I> ? I : never : never;
}

export default Aisles

我希望,這具有您關心的大多數功能。 您可以訪問AislesnameaisleId鍵:

Aisles.food.someMethod(); 
// "Hi, I'm food and my Id is 4 and my description is All things edible"
Aisles[7].someMethod(); 
// "Hi, I'm housewares and my Id is 7 and my description is Make your home zing"

您可以使用Aisles類型作為可區分聯合:

function processAisle(aisle: Aisles) {
    switch (aisle.name) {
        case "camping": return 0;
        case "food": return 1;
        // not all paths return a value unless you uncomment next line
        /* case "housewares": return 2; */
    }
}

您可以使用導出的Aisles.Name類型將鍵限制為僅name而不是aisleId屬性:

function reImplementGetAisleByName<N extends Aisles.Name>(name: N) {
    return Aisles[name]
}
const camping = reImplementGetAisleByName("camping");
// const camping: Aisle<"camping", "9">

Playground 鏈接到代碼

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM