映射類型 - 如何在 Typescript 中強制屬性之間的相互依賴
我正在嘗試在一般的 TypeScript 類型中強制屬性之間的相互依賴。上下文是一個可重用的 React 元件,它接收 props。例如,如果元件接收到按鈕的文字,那麼它也必須接收到該按鈕的處理函數;但如果元件沒有接收到文字,那麼處理函數的 prop 也應該為 undefined。
這是我的嘗試:
type InterConnectedProps<
T extends string | undefined,
PropertyName extends string | undefined,
> = T extends string
? ({
[K in T]: string;
} & { [K in PropertyName]: () => void })
: {
T?: undefined;
PropertyName?: undefined;
};
//Alternatively:
type InterConnectedProps2<
T extends string | undefined,
PropertyName extends string | undefined,
> = T extends string
? ({
[K in T]: string;
//The differnce lies here: "as string"
} & { [K in PropertyName as string]: () => void })
: {
T?: undefined;
PropertyName?: undefined;
};
type Props = {
title: string;
subtitle?: string;
}& InterConnectedProps2<'button1', 'handlerBtn1'> & InterConnectedProps2<'button2', 'handlerBtn2'>;
// Props should be either:
// {title: string; subtitle?:string; button1: string; handlerBtn1 : () => void}
// or {title: string; subtitle?:string; button1: undefined; handlerBtn1 : undefined}
//Anyway, there should be no handlerBtn1 if there is no button1
但是這不起作用,不僅是因為我沒有驗證 PropertyName 是字串(我該如何引入多重條件?)。即使我寫 { [K in PropertyName as string]:() => void },結果的類型似乎也不包含第二個參數(PropertyName):
type Button=InterConnectedProps<'button1', 'btn1Handler'> 只是 {button1: string}。
我哪裡做錯了,還是有其他的選擇?
謝謝!
答案#1
看起來你想要定義一個工具型別 AllOrNone<T>,其中 T 是一個具有所有必填屬性的物件型別,而其結果型別將接受一個 T 型別的物件,或一個不包含 T 的任何屬性的物件。
這裡有一種可能的定義方式:
type AllOrNone<T extends object> =
T | { [P in keyof T]?: never }
這是一個聯合類型,用來表示「二擇一」的部分。一個聯合成員是 T,另一個是 { [P in keyof T]?: never },這在 TypeScript 中是我們能盡可能接近「一個不包含 T 任何屬性的物件」的方式。
讓我們更詳細地檢視 { [P in keyof T]?: never }。這是一個映射類型,其中 T 的每個屬性鍵都被設為可選,而值的類型是不可能的 never 類型。因此,如果 T 是 {a: 0, b: 1, c: 2},那麼 {[P in keyof T]?: never} 就是 {a?: never, b?: never, c?: never}。TypeScript 並沒有真正的方式來 禁止 一個屬性。但是,型別為 never 的可選屬性接近於禁止:你無法提供 never 類型的值,因此你唯一的“選擇”就是完全不提供這個屬性(或者根據你的編譯器設定將其設為 undefined)。
讓我們來測試一下:
type Z = AllOrNone<{ a: 0, b: 1, c: 2 }>;
/* type Z = { a: 0, b: 1, c: 2 } |
{ a?: never, b?: never, c?: never } */
let z: Z;
z = {a: 0, b: 1, c: 2}; // okay
z = {a: 0, b: 1}; // error!
z = {a: 0}; // error!
z = {}; // okay
這有道理;Z 要麼是 {a: 0, b: 1, c: 2},要麼是一個沒有這些屬性的物件。
現在我們可以使用兩次 AllOrNone 來定義 Props:
type Props = { title: string; subtitle?: string; } &
AllOrNone<{ button1: string, handlerBtn1(): void }> &
AllOrNone<{ button2: string, handlerBtn2(): void }>;
let p: Props;
p = { title: "" };
p = { title: "", button1: "", handlerBtn1() { } };
p = { title: "", button2: "", handlerBtn2() { } };
p = { title: "", button1: "", handlerBtn2() { } }; // error
看起來不錯!
程式碼的 Playground 連結
