TypeScript 的 Union Type 与属性互斥

513 阅读3分钟

题目

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编码的东西来解决

数据库逻辑设计之三大范式通俗理解,一看就懂,书上说的太晦涩 - SegmentFault 思否

答案

简单场景推荐重载, 直观, 错误提示友好, 且可以避免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>}`;

补充

不同参数不同返回值

blog.gplane.win/posts/condi…

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

juejin.cn/post/702585…

生成union type

www.zhihu.com/question/47…

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

zhuanlan.zhihu.com/p/58704376