联合类型转交叉类型

January 16, 2022

在 TypeScript 中,可以利用 Distributive conditional typesinfer 将联合类型转换为交叉类型。

type UnionToIntersection<T> = (T extends any ? (x: T) => void : never) extends (
  x: infer U
) => void
  ? U
  : never

type A = { a: string }
type B = { b: number }

type Res = UnionToIntersection<A | B> // A & B

具体来说,当向 UnionToIntersection<T> 传入 A | B 时:

首先,根据 Distributive conditional types 规则,T extends any ? (x: T) => void : never 会被处理成 A extends any ? (x: T) => void : never | B extends any ? (x: T) => void : never,即为 (x: A) => void | (x: B) => void

其次,此时 AB 均在逆变位置(函数参数)上,结合同一类型变量在逆变位置的多个候选类型将会被推断为交叉类型可以推断出 UA & B

Assertion Function

December 29, 2021

TypeScript 3.7 版本发布了一个新概念——断言函数,它的作用与 Node.js 中的 assert() 断言函数类似。

为此,TypeScript 引入了一个新的断言签名 asserts 以标记一个函数是断言函数。

function assert(condition: any): asserts condition {
  if (!condition) {
    throw new Error()
  }
}

asserts condition 可以理解为如果 assert() 函数执行完毕并返回,此时 condition 参数必须是真值,否则函数将抛出错误。

TypeScript 运行时能够利用断言函数对变量类型进行收窄,相比于其他收窄方式,断言函数更具有表达性。

function foo(x: string | number) {
  assert(typeof x === 'string')
  x.toLocaleUpperCase() // x 被收窄为 string 类型

  assert(typeof x === 'number')
  x // 无法再收窄类型,此时 x 为 never 类型
}

结合 assertsis 标记可以更清晰地收窄某个变量为指定类型。

function assertIsNumber(val: any): asserts val is number {
  if (typeof val !== 'number') {
    throw new Error()
  }
}

function foo(x: string | number) {
  assertIsNumber(x)

  x // x 被收窄为 number 类型
  x.toUpperCase() // error, 类型 “number” 上不存在属性 “toUpperCase”
}

asserts val is number 表示如果 val 不是 number 类型,函数将抛出错误。

根据这个特性,我们可以写出一些复杂的类型判断。例如,判断某个变量是否为 nullundefined 类型,如果不是就收窄其类型,如果是就抛出错误。

function assertIsDefined<T>(
  val: T,
  msg?: string
): asserts val is NonNullable<T> {
  if (typeof val === 'undefined' || val === null) {
    throw new Error(msg)
  }
}

function bar(x?: string | null) {
  assertIsDefined(x, 'Param x should not be null or undefined.')

  x.toUpperCase() // x 被收窄为 string 类型
}

bar() // Param x should not be null or undefined.

for await...of

December 26, 2021

const promises = [
  () => Promise.resolve(0),
  Promise.resolve(1),
  Promise.reject(2),
  Promise.resolve(3),
]
;(async function () {
  try {
    for await (let v of promises) {
      if (typeof v === 'function') {
        v = await v()
      }

      console.log(v)
    }
  } catch (e) {
    console.log('caught', e)
  }
})()
// 0
// 1
// caught 2

该段代码等价于以下代码:

;(async function () {
  try {
    for (let v of promises) {
      if (typeof v === 'function') {
        v = v()
      }

      console.log(await v)
    }
  } catch (e) {
    console.log('caught', e)
  }
})()

for...of 的不同的是,for await...of 创建的循环不仅可以遍历实现同步迭代协议的对象,还可以用于实现异步迭代协议的对象。

Structured cloning API

December 20, 2021

最近 HTML 标准新增了 Structured cloning API 用于克隆操作。它是一种结构化克隆,不但支持多数内置类型,而且支持循环引用的对象。

structuredClone(value)
structuredClone(value, { transfer })

// 复制循环引用的对象
const original = { name: 'structuredClone' }
original.itself = original

const clone = structuredClone(original)

console.log(clone === original) // false
console.log(clone.itself === clone) // true
console.log(clone.itself === original.itself) // false

其次,Structured cloning API 还支持部分引用类型的值传递(原始对象中属性值传递(非复制)到新对象后,原始对象无法再访问该属性值)。

// 值传递
const original = new Uint8Array(1024)
const transferred = structuredClone(original, { transfer: [original.buffer] })

console.log(transferred.byteLength) // 1024
console.log(transferred[0]) // 1
console.log(original.byteLength) // 0

目前只有部分最新的浏览器版本和 node.js 17 以上版本支持。

import.meta

December 10, 2021

我在排查 bug 过程中,偶然间在一个依赖包源码中发现了这段代码:

const worker = new Worker(new URL('./xxx.js', import.meta.url))

脑中检索了一遍,完全没有印象……于是阅读了相关的文档。

原来 import.meta 是一个由 null 作为原型生成的对象,包含该 JavaScript 模块的上下文信息。它有一个非常有用的属性 url,表示该 JavaScript 模块的引用路径,可以携带 query parameters 或者 hash。

<script type="module">
  import './a.js?id=1'
  import './b.js#1'
</script>
// a.js
console.log(Object.getPrototypeOf(import.meta)) // null

console.log(import.meta) // { url: "file:///home/user/a.js?id=1" }

// b.js
console.log(import.meta) // { url: "file:///home/user/b.js#1" }

node 环境也适用,前提是必须支持 JavaScript 模块语法。