[英]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.Food
或Aisles['7']
會很好。 雖然上述getBy
方法可用於,這不是美妙必須使用Aisles.getById('food')
時, 'food'
,在編譯時是已知的。 如果這些必須在不同的枚舉中,例如AisleNames.Food
或AisleIds['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>
和aisleId
的Map
遇到了問題,即使類型聯合用於傳入的值, Map<K, V>#get
方法返回類型V | undefined
V | undefined
,需要某種解決方法來誘使 TypeScript 相信只要值在聯合類型中就知道結果不是undefined
。 我知道如何使用返回var is Type
作為返回值的函數,或者asserts var is Type
,但寧願不將它們暴露給這些對象的使用者——這都應該是Aisles
和Aisle
枚舉的實現的內部構造,如果可能的話。
如有必要,我可以為每個Aisle
編寫一個單獨的具體類。 這很好,並且可以讓我在每個中對aisleName
和aisleId
進行硬編碼, aisleName
aisleId
這些值傳遞給構造函數。 但是“如何在一個作為可區分聯合的集合中使用這些”問題變得更糟。
有沒有人有任何想法?
為了能夠區分類型的聯合,在具有文字值(布爾值、字符串、數字或唯一符號)的類型之間必須至少有一個公共屬性。 通過向Aisle
添加T extends ...
屬性,您走上了正確的道路。
這是一種可能的方法,它從您的Aisle
類中提取id
和name
參數以獲得最大的編譯時推理能力:
// `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
,后者只是引擎蓋下的類實例。 如果您想要類似枚舉的類實例,則必須自己構建它們。
一種方法可能是不出口的基礎類(這樣新情況下,你不控制不到處露面),並確保他們是強類型足以使產生的枚舉可以表現為一個可識別聯合由具有字面- 值的判別屬性。 看起來您有兩個這樣的屬性, name
和aisleId
,所以讓我們在這兩個中使這個底層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
枚舉對象具有這些name
和aisleId
值中的每一個的鍵,我們可以編寫一個輔助函數,該函數返回一個具有這兩個鍵的對象,其中每個值都是相關的類實例:
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
; 例如,相關鍵的並集對應的Name
和Id
類型:
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
我希望,這具有您關心的大多數功能。 您可以訪問Aisles
的name
和aisleId
鍵:
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">
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.