[英]Typescript Issue on Discriminated Union
我正在嘗試編寫一個帶有區分聯合的簡單工廠 function 來創建 class 的新實例。 但是我還沒有找到任何滿足我要求的方法。
import { v4 as uuidv4 } from 'uuid';
type Gender = "male" | "female" | "nonBinary";
type Platform = "telegram" | "whatsapp";
type City = "myLocation" | "amsterdam" | "barcelona" | "berlin" | "brussels" | "buenosAires" | "capeTown" | "copenhagen" | "chicago" | "dubai" | "gothenburg" | "helsinki" | "hongKong" | "istanbul" | "kualaLumpur" | "kyiv" | "lisbon" | "london" | "losAngeles" | "madrid" | "malmo" | "manchester" | "melbourne" | "milan" | "montreal" | "newYork" | "oslo" | "paris" | "reykjavik" | "rio" | "rome" | "sanFrancisco" | "saoPaulo" | "singapore" | "stockholm" | "sydney" | "toronto" | "vienna" | "zurich";
type BodyType = "thin" | "athletic" | "average" | "extraPounds" | "bigTall";
type Education = "none" | "highSchool" | "college" | "bachelors" | "masters" | "phDPostDoctoral";
interface IUser {
userId?: string;
name: string;
age: number;
gender: Gender;
}
class User implements IUser {
readonly userId: string;
readonly name: string;
readonly age: number;
readonly gender: Gender;
constructor(props: { name: string, age: number, gender: Gender, userId?: string }) {
this.userId = props.userId || uuidv4();
this.name = props.name
this.age = props.age
this.gender = props.gender
}
public get ID(): string | undefined {
return this.userId;
}
}
interface TelegramMeta {
telegramId: number;
job: string;
bodyType?: BodyType,
height?: number,
children?: boolean,
smoker?: boolean,
education?: Education,
educationName?: string,
about?: string,
favouriteCities?: string,
placesToGo?: string,
freeTime?: string
}
interface ITelegramUser extends IUser {
platform: "telegram",
telegramMeta: TelegramMeta,
}
class TelegramUser extends User implements ITelegramUser {
readonly platform = "telegram"
telegramMeta: TelegramMeta;
constructor(props: { name: string, age: number, gender: Gender, telegramMeta: TelegramMeta, userId?: string }) {
super(props);
this.telegramMeta = props.telegramMeta;
}
}
interface WhatsappMeta {
whatsappId: string,
intagramLinks?: string[]
}
interface IWhatsappUser extends IUser {
platform: "whatsapp",
whatsappMeta: WhatsappMeta,
}
class WhatsappUser extends User {
readonly platform = "whatsapp"
whatsappMeta: WhatsappMeta;
constructor(props: { name: string, age: number, gender: Gender, whatsappMeta: WhatsappMeta, userId?: string }) {
super(props);
this.whatsappMeta = props.whatsappMeta;
}
}
type UserTypes = ITelegramUser | IWhatsappUser;
type UserPlatforms = UserTypes['platform'];
type ReturnTypeLookup = {
"telegram": ITelegramUser;
"whatsapp": IWhatsappUser;
};
type SchemaFor<T extends UserTypes> = ReturnTypeLookup[T['platform']];
export function createUser<T extends UserTypes>(data: T): SchemaFor<T> {
if (data.platform === 'telegram') {
return new TelegramUser(data);
}
return new WhatsappUser(data);
}
const telegramUser = createUser<ITelegramUser>({
name: "Harrison",
age: 30,
gender: "male",
platform: "telegram",
telegramMeta: {
telegramId: 123,
job: 'Developer'
},
});
它給出了這個錯誤信息:
Type 'TelegramUser' is not assignable to type 'SchemaFor<T>'.
Type 'TelegramUser' is not assignable to type 'never'.
The intersection 'ITelegramUser & IWhatsappUser' was reduced to 'never' because property 'platform' has conflicting types in some constituents.
TLDR 示例: https://tsplay.dev/wEPMyW
讓我解釋一下您要做什么以及遇到的問題。 你有一個受歧視的工會:
interface Foo {
type: "foo",
fooMeta: { a: string }
}
interface Bar {
type: "bar",
barMeta: { b: number }
}
type Types = Foo | Bar;
你有一些功能,map 這個聯合的每個成員到一些相應的 output 類型:
declare function doFoo(data: Foo): number;
declare function doBar(data: Bar): Date;
您想將 package 這些轉換為單個通用function 接受可區分聯合的任何成員並委托給相應的 function',並返回相應的函數,
type ReturnLookup = {
"foo": number;
"bar": Date;
};
declare function create<T extends Types>(data: T): ReturnLookup[T['type']];
const doneFoo = create({ type: "foo", fooMeta: { a: "" } }); // number
const doneBar = create({ type: "bar", barMeta: { b: 123 } }); // Date
但是,不幸的是,您似乎無法在不出現類型錯誤的情況下實現此 function:
export function create<T extends Types>(data: T): ReturnLookup[T['type']] {
if (data.type === 'foo') {
return doFoo(data) // number is not assignable to number & Date
}
return doBar(data) // Date is not assignable to number & Date
}
為什么會發生這種情況,如何解決?
發生這種情況是因為當您檢查類型T
的值data
時編譯器無法縮小類型參數T
。 檢查data.type
縮小data
范圍,但不會影響T
本身。 由於T
在 function 實現中不受影響,編譯器永遠不知道您應該返回 doFoo 的返回類型doBar
doFoo
的返回類型。 它唯一認為安全的是,如果您以某種方式返回了兩種類型的值,即兩種類型的交集。 這就是為什么您會收到有關number
或Date
不能分配給交叉點number & Date
的錯誤。
GitHub 中有各種問題提到了這個問題並請求修復它的方法。 一個是microsoft/TypeScript#33014 ,其中建議有一些方法可以縮小泛型類型參數T
。
另一個是microsoft/TypeScript#30581 ,它處理相關的聯合類型,如果您願意重構,則在microsoft/TypeScript#47109上針對此問題的推薦修復提供了前進的方向:
重構是將您的可區分聯合更改為 object 類型,其鍵是判別屬性,其屬性值為成員的 rest:
interface TypeMapping {
foo: {
fooMeta: { a: string }
},
bar: {
barMeta: { b: number }
}
}
然后,不是將Types
定義為聯合,而是將其定義為分布式 object 類型,在其中創建映射類型並立即對其進行索引以獲得聯合:
type Types<K extends keyof TypeMapping = keyof TypeMapping> =
{ [P in K]: { type: P } & TypeMapping[P] }[K]
type Foo = Types<"foo">
type Bar = Types<"bar">
Foo
、 Bar
和Types
類型等價於頂部的類型(請注意,沒有泛型參數的Types
由於default計算為Types<keyof TypeMapping>
),但現在編譯器明確地看到了Types<K>
之間的關系Types<K>
和K
的工會成員。
然后我們可以將doFoo
和doBar
函數合並到一個 object 中,其類型也明確表示為TypeMapping
的鍵:
const funcs: { [P in keyof TypeMapping]: (data: Types<P>) => ReturnLookup[P] } = {
foo: doFoo,
bar: doBar
}
這種表示很重要。 它允許編譯器理解,對於泛型K
的Types<K>
的值data
,您可以調用funcs[data.type](data)
並獲取 output 類型ReturnLookup[K]
:
export function create<K extends keyof TypeMapping>(
data: Types<K>
): ReturnLookup[K] {
return funcs[data.type](data); // okay
}
const doneFoo = create({ type: "foo", fooMeta: { a: "" } }); // number
const doneBar = create({ type: "bar", barMeta: { b: 123 } }); // Date
所以那里沒有錯誤,萬歲
但請注意,它起作用的原因與此處類型的確切表示有很大關系。 例如,如果您刪除上面funcs
的注釋:
const funcs = {
foo: doFoo,
bar: doBar
}
然后錯誤又回來了:
return funcs[data.type](data); // error, number | Date not number & Date
那是因為編譯器失去了funcs[data.type]
的類型和data
的類型之間的聯系(這是 microsoft/TypeScript#30581 的全部主題),你可能根本沒有重構。
所以要小心這種技術
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.