TypeScript 的 Union Type 與屬性互斥
題目
TS Playground(閱讀原文,鏈接跳轉)
/**
*
* 導出了一個 defineConfig 的函數給開發者,其中有 a 和 b 字段是二選一的, foo 是可選的
* 完成 UserConfig
*
*/
interface UserConfig {}
function defineConfig(t: UserConfig) {
//
}
defineConfig({ a: true })
defineConfig({ b: true })
defineConfig({ a: true, b: true })
defineConfig({ a: true, c: true })
發散一下: 支持自定義互斥字段, 以及多組互斥字段
想法
體操的最終目的是爲了用戶寫 ts 的時候像 js 一樣, 使用的時候儘量少感知類型的存在。
樹和遞歸
一個簡單例子, 在使用的時候完全不需要寫類型
interface TreeNode<T> {
value: T;
left?: TreeNode<T>;
right?: TreeNode<T>;
}
function toList<T>(root: TreeNode<T>) {
const list: T[] = [];
function dfs(node: TreeNode<T>) {
if (!node) {
return;
}
if (node.left) {
dfs(node.left);
}
if (node.right) {
dfs(node.right);
}
list.push(node.value);
}
dfs(root);
return list;
}
declare function createTree<T>(list: T[]): TreeNode<T>;
const numNode = createTree([1]);
const strNode = createTree(["1"]);
const numList = toList(numNode);
const strList = toList(strNode);
避免錯誤設計
對於這種多字段互斥的場景其實是違反三範式的, 類型是對數據的抽象, 數據最後在保存的時候基本上都是一個二維表, 如果兩個字段互斥, 說明該表有冗餘, 應該合併. 否則只能用 ts 自己實現類似 One-hot 編碼的東西來解決。
https://segmentfault.com/a/1190000013695030
答案
簡單場景推薦重載, 直觀, 錯誤提示友好, 且可以避免 undefined 的問題
答案一: 手工加互斥字段
/**
*
* 導出了一個 defineConfig 的函數給開發者,其中有 a 和 b 字段是二選一的, foo 是可選的
* 完成 UserConfig
*
*/
interface CommonConfig {
foo?: string;
}
interface JsConfig extends CommonConfig {
a: boolean;
b?: never;
}
interface TsConfig extends CommonConfig {
b: boolean;
a?: never;
}
type UserConfig = JsConfig | TsConfig
function defineConfig(t: UserConfig) {
//
}
// ok
defineConfig({ a: true })
defineConfig({ a: true, foo: 'aa' })
defineConfig({ b: true })
// error
defineConfig({ a: true, b: true })
defineConfig({ a: true, c: true })
答案二: 函數重載
/**
*
* 導出了一個 defineConfig 的函數給開發者,其中有 a 和 b 字段是二選一的, foo 是可選的
* 完成 UserConfig
*
*/
interface CommonConfig {
foo?: string;
}
interface JsConfig extends CommonConfig {
a: boolean;
}
interface TsConfig extends CommonConfig {
b: boolean;
}
type UserConfig = JsConfig | TsConfig
function defineConfig(c: JsConfig): void;
function defineConfig(c: TsConfig): void;
function defineConfig(t: UserConfig) {
//
}
// ok
defineConfig({ a: true })
defineConfig({ a: true, foo: 'aa' })
defineConfig({ b: true })
// error
defineConfig({ a: true, b: true })
defineConfig({ a: true, c: true })
答案三: 體操自動加字段
將類型的 key 劃分爲兩組, 一組使用原始值, 一組使用 never, 然後用 & 拼裝爲一個新類型
/**
*
* 導出了一個 defineConfig 的函數給開發者,其中有 a 和 b 字段是二選一的, foo 是可選的
* 完成 UserConfig
*
*/
type UserConfig = {
foo?:string,
a:boolean,
b:boolean,
c: boolean;
}
{a:boolean} & {foo?: string} & {b:never, c : never}
{b:boolean} & {foo?: string};
{c:boolean} & {foo?: string};
type SetKeyNever<T, K extends keyof T> = {
[x in K]?: never;
};
// type z = SetKeyNever<UserConfig, 'a' | 'b'>
type z = Pick<UserConfig, 'a' | 'b'>
type x = Exclude<'a' | 'v', 'a'>
type JustOne<T, K extends (keyof T)[] = [], Y extends keyof T = K[number]> = {
[x in Y]: Pick<T, Exclude<keyof T, Exclude<Y, x>>> &
SetKeyNever<T, Exclude<Y, x>>;
}[Y];
type n = ['a', 'b','c'][number]
function defineConfig(t: JustOne<UserConfig, ['a', 'b','c']>) {
//
}
// ok
defineConfig({ a: true })
defineConfig({ a: true, foo: 'aa' })
defineConfig({ b: true })
// error
defineConfig({ a: true, b: undefined })
defineConfig({ a: true, c: true })
type z1 = JustOne<UserConfig, ['a', 'b','c']>
type MergeIntersection<T> = T extends object ? {
[K in keyof T]: T[K] extends object ? MergeIntersection<T[K]> : T[K]
} : T
// 可視化
type p = MergeIntersection<z1>
答案四: XOR
/**
*
* 導出了一個 defineConfig 的函數給開發者,其中有 a 和 b 字段是二選一的, foo 是可選的
* 完成 UserConfig
*
*/
// type XOR<T, A, B> = T extends A ? A : B;
// type foo = { [key: string]: never, a: boolean, };
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;
type UserConfig = XOR<{ a: boolean}, { b: boolean }> & { foo?: string }
function defineConfig(t: UserConfig) {
//
}
// ok
defineConfig({ a: true })
defineConfig({ a: true, foo: 'aa' })
defineConfig({ b: true })
// error
defineConfig({ a: true, b: true })
defineConfig({ a: true, c: true })
// type MergeIntersection<T> = T extends object ? {
// [K in keyof T]: T[K] extends object ? MergeIntersection<T[K]> : T[K]
// } : T
常見問題
declare function f1(c: { a: boolean }): void;
declare function f2(c: { a: boolean } | { c: boolean }): void;
// error
f1({ a: true, c: true });
// ok
f2({ a: true, c: true });
type A = { a: boolean };
// error
const y: A = { a: true, b: true };
const z = { a: true, b: true } as const;
// ok
const x: A = z;
union 的使用場景
非常適合做笛卡爾積
type EventName = "click" | "mousedown" | "mouseup";
type HandleType = "on" | "off";
type EventFn = `${HandleType}${Capitalize<EventName>}`;
補充
不同參數不同返回值
https://blog.gplane.win/posts/conditional-type-in-ts.html
type Option = {
token?: boolean;
};
declare function f<T extends Option>(
c?: T
): T["token"] extends true ? { token: number[] } : string;
f({ token: true }).token.sort();
f({ token: false }).token.sort();
f().token.sort();
體操 EitherOr
https://juejin.cn/post/7025851349103280164
生成 union type
https://www.zhihu.com/question/47452733
interface A {
a: { a: number };
b: { b: string };
c: { c: boolean };
}
type AddKind<A> = {
[K in keyof A]: A[K] & { kind: K };
};
type Intermediate = AddKind<A>;
type IntermediateWithKind = Intermediate[keyof Intermediate]
union 轉 tuple
https://zhuanlan.zhihu.com/p/58704376
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/INiOZANq2kBWQxF_-pksMg