MuYunyun / advanced-typescript

MIT License
5 stars 0 forks source link

note

用于条件判断时的 extends

当 extends 用于表示条件判断时,可以总结出以下规律

  1. 同一类型的子类型在使用 extends 时,extends 语义可解释为等于。
type result1 = 'a' extends 'abc' ? true : false // false
type result2 = 123 extends 1 ? true : false // false
  1. 狭窄类型 extends 宽泛类型宽泛类型中包含狭窄类型时结果为 true,反之为 false。
type result3 = string extends string | number ? true : false // true
  1. 当 extends 作用于对象时,若在对象中指定的 key 越多,则其类型定义的范围越狭窄,可以参考如下例子。
type result4 = { a: true, b: false } extends { a: true } ? true : false // true

泛型类型中的 extends

考虑如下 Demo 类型定义:

type Demo<T, U> = T extends U ? never : T

因为 'a' | 'b' | 'c' extends 'a' 是 false, 所以 Demo<'a' | 'b' | 'c', 'a'> 结果是 'a' | 'b' | 'c' 么?

查阅官网,其中有提到:

When conditional types act on a generic type, they become distributive when given a union type.

即当条件类型作用于泛型类型时,它们在给定联合类型时成为分配类型。用 JavaScript 来表达 'a' | 'b' | 'c' extends 'a' 的结果类似于:

function Demo(T, U) {
  return T.map(val => {
    if (val !== U) return val
    return 'never'
  })
}

Demo(['a', 'b', 'c'], 'a') // ['never', 'b', 'c']

此外根据 never 类型的定义 —— never 类型可分配给每种类型,但是没有类型可以分配给 never(除了 never 本身)。即 never | 'b' | 'c' 等价于 'b' | 'c'

因此 Demo<'a' | 'b' | 'c', 'a'> 结果并不是 'a' | 'b' | 'c' 而是 'b' | 'c'。而 Demo 类型的声明其实就是 TS 官方提供的 Exclude<Type, ExcludedUnion>

逃离舱

如果想让 Demo<'a' | 'b' | 'c', 'a'> 的结果为 'a' | 'b' | 'c' 是否可以实现呢? 根据官网描述:

Typically, distributivity is the desired behavior. To avoid that behavior, you can surround each side of the extends keyword with square brackets.

如果不想遍历泛型中的每一个类型,可以用方括号将 extends 关键字的每一侧括起来。

type Demo<T, U> = [T] extends [U] ? never : T

// result 此时类型为 'a' | 'b' | 'c'
type result = Demo<'a' | 'b' | 'c', 'a'>

在箭头函数中使用 extends

在箭头函数中使用三元表达式时,从左向右的阅读习惯导致函数内容区若不加括号则会让使用方感到困惑。比如下方代码中 x 是函数类型还是布尔类型呢?

// The intent is not clear.
var x = a => 1 ? true : false;

在 eslint 规则 no-confusing-arrow 中,推荐如下写法:

var x = a => (1 ? true : false);

在 TypeScript 的类型定义中若在箭头函数中使用 extends 也是同理,一眼看如下代码也是比较绕,

Todo: 换一个 Demo

type CurryV0<P extends any[], R> =
  (arg0: Head<P>) => HasTail<P> extends true ? CurryV0<Tail<P>, R> : R

因此在箭头函数中使用 extends 建议加上括号,对 code review 很有帮助。

type CurryV0<P extends any[], R> =
  (arg0: Head<P>) => (HasTail<P> extends true ? CurryV0<Tail<P>, R> : R)

结合类型推导使用 extends

TypeScript 中有一个语法 [infer](),使用它可以实现推导类型的目的。一般会结合 extends 来使用 infer。比如:

todo:

使用 extends 来判断两个类型完全相等

// https://github.com/Microsoft/TypeScript/issues/27024#issuecomment-421529650. understanding it is difficult.
export type Equal<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<U>() => U extends Y ? 1 : 2) ? true : false

将 tuple 转化为对象

可以利用 tuple[number] 取得 tuple 的并集。

const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const
type test = typeof tuple[number] // "tesla" | "model 3" | "model X" | "model Y"

可以利用 P in T[number] 进行遍历。

type TupleToObject<T extends readonly any[]> = {
  [P in T[number]]: string
}

在对象中使用交、并集

在对象中使用 |&,与在非对象中使用存在语义上的差异。

在集合对象中使用联合类型 | ,官网 working-with-union-types 有如下说明:

Notice that given two sets with corresponding facts about each set, only the intersection of those facts applies to the union of the sets themselves.

type Foo = {
  name: string
  age: string
}
type Bar = {
  name: string
  age: string
  gender: number
}

type result = keyof (Foo | Bar) // "name" | "age"

在集合对象中使用交集类型 & ,可以见 intersection-types 给出的 demo:

interface Colorful {
  color: string;
}
interface Circle {
  radius: number;
}

type ColorfulCircle = keyof (Colorful & Circle) // "color" | "radius"

结合 &| 的使用,我们能立马写出比如类型 diff

type Diff<O, O1> = Omit<O & O1, keyof (O | O1)>

type Foo = {
  name: string
  age: string
}
type Bar = {
  name: string
  age: string
  gender: number
}

type result = Diff<Foo, Bar> // { gender: number }

使用 declare 声明

使用 declare 声明,可以将类型可以收拢在一处定义。使用 declare 可以定义以下类型:

declare const foo: number;
declare function greet(greeting: string): void;

注意的是,使用 declare 声明的变量与函数指向到具体的值,若要获取其类型定义,则需要使用 typeof。

type fooType = typeof foo
type greetType = typeof greet
declare namespace GreetingLib {
  interface A {}
  interface B {}
}

link

Todo