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> = (| 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