Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TypeScript 中的 never、unknown 的使用时机 #84

Open
AaronConlon opened this issue Sep 7, 2024 · 0 comments
Open

TypeScript 中的 never、unknown 的使用时机 #84

AaronConlon opened this issue Sep 7, 2024 · 0 comments
Labels
编程 软件开发的旅途

Comments

@AaronConlon
Copy link
Owner

AaronConlon commented Sep 7, 2024


description: 最近面试时被提问到 TypeScript 相关的知识,其中一个问题就是:什么时候应该在 TypeScript 中使用 never 和 unknown。
cover: https://de4965e.webp.li/blog-images/2024/09/1d13fb693096a714f5a96a44fc567a22.png

最近面试时被提问到 TypeScript 相关的知识,其中一个问题就是:什么时候应该在 TypeScript 中使用 neverunknown

笔者觉得自己没答好,于是再次学习了相关知识分享下来。本文不是 TypeScript types 的科普,所以别着急,只需一步一个脚印前进即可。

TypeScript 类型浅谈

首先,我们要明确什么是 TypeScript 的类型。

简单说,string类型即 TypeScript 中字符串变量的类型集合。同理,number是数字变量的类型集合,当我们写下如下代码时:

const stringOrNumber: strig | number = '1';

联合类型 string | number应运而生,这个联合类型就是字符串类型集合和数字类型集合的并集。

我们在 TypeScript 中可以把字符串类型或数字类型的变量赋值给 stringOrNumber,除此之外还能把什么类型的变量赋值给 stringOrNumber呢?

答案是:

  • never
  • any

😁 我们不谈 any类型,至少在离职之前不谈 any类型,也不在项目中使用 any类型(如果加班很多当我没说)。

never类型是一个空集,它处于类型系统的最内层。

当我们定一个变量的类型是 unknown时,我们实际上需要的就是一个未知类型的值,举个例子:

let foo: unknown;
foo = stringOrNumber; // ok

function demo(param: unknown) {
  // ...
  if(typeof param === 'string') {
    // ...
  }
}
demo(stringOrNumber); // ok

unknown是所有类型集合的超集,never则是所有类型的子集(bottom type)。

never 类型浅析

来点代码,再谈其他:

function asyncTimeout(ms: number): Promise<never> {
  return new Promise((_, reject) => {
    setTimeout(() => reject(new Error(`Async Timeout: ${ms}`)), ms)
  })
}

上述代码是一个异步超时的函数,其返回值类型是 Promise<never>,接着我们继续实现一个支持超时的请求函数:

function fetchData(): Promise<{ price: number }> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ price: 100 });
    }, Math.random() * 5000);
  });
}

async function fetchPriceWithTimeout(tickerSymbol: string): Promise<number> {
  const stock = await Promise.race([
    fetchData(), // returns `Promise<{ price: number }>`
    asyncTimeout(3000), // returns `Promise<never>`
  ]);
  // stock is now of type `{ price: number }`
  return stock.price;
}

Promise.race 是 JavaScript 中 Promise 对象的一个静态方法,它用于处理多个 Promise 实例,并返回一个新的 Promise 实例。这个新的 Promise 实例会在第一个传入的 Promise 实例完成或拒绝时完成或拒绝。

fetchData是模拟的数据请求函数,实际开发中可以是某个接口请求,TypeScript 编译器将 race函数的返回值类型视为 Promise<{price: number}> | Promise<never>stock的类型为 {price: number},这就是 never类型的使用场景实例。

再来一个代码例子(来源于网络):

type Arguments<T> = T extends (...args: infer A) => any ? A : never
type Return<T> = T extends (...args: any[]) => infer R ? R : never

function time<F extends Function>(fn: F, ...args: Arguments<F>): Return<F> {
  console.time();
  const result = fn(...args);
  console.timeEnd();
  return result;
}

function add(a: number, b: number): number {
  return a + b;
}

const sum = time(add, 1, 2); // 调用 time 函数测量 add 函数的执行时间

console.log(sum); 
// 输出:
// [0.09ms] default
// 3

上述示例使用了 never类型来缩窄条件类型的结果。在 type Arguments<T>中,如果传入的泛型 T是一个函数,则将从其参数的类型生成一个推断类型 A作为满足条件时的类型。也就是说,如果我们传入的不是函数,那么TypeScript 编译器最后就会得到 never类型兜底。

于是乎在 time函数定义的时候就可以使用这两个带泛型的条件类型定义,最终让 time函数能够通过传入的函数 add的类型来推导最终的返回值类型:sumnumber类型。

我不确定大家工作中是否要写这些类型定义,但这就是 TypeScript。

我们可以在官方源代码中看到类似的类型声明:

/**
 * Exclude null and undefined from T
 */
type NonNullable<T> = T extends null | undefined ? never : T;

这里的条件类型对于传入泛型是联合类型的时候,会将判断类型分发到每一个到联合类型。

// if T = A | B

T extends U ? X : T == (A extends U ? X : A) | (B extends U ? X : B)

所以 NonNullable<string | null>的解析逐层如下:

NonNullable<string | null>
  // The conditional distributes over each branch in `string | null`.
  == (string extends null | undefined ? never : string) | (null extends null | undefined ? never : null)

  // The conditional in each union branch is resolved.
  == string | never

  // `never` factors out of the resulting union type.
  == string

最终编译器识别其为 string类型。

unknown 类型浅析

任何类型的值都可以赋值给 unknown类型的变量,因此我们可以在考虑使用 any的时候优先考虑 unknown,一旦我们选择使用 any,也就意味着我们主动关闭了 TypeScript的类型安全保护。

好了,下面来看一个代码示例:

function prettyPrint(x: unknown): string {
  if (Array.isArray(x)) {
    return "[" + x.map(prettyPrint).join(", ") + "]"
  }
  if (typeof x === "string") {
    return `"${x}"`
  }
  if (typeof x === "number") {
    return String(x)
  } 
  return "etc."
}

这是一个美观的打印输出函数,在调用时可以传入任意类型的参数。但是在传入后我们需要手动去缩窄类型,才能在逻辑内部使用特定类型变量的方法。

此外,如果数据来源不明确时,也可以促使开发者进行必要的类型验证和类型检查,减少运行时错误。

另外,笔者还见到了如下所示的类型守卫示例,稍微再分享一下:

function func(value: unknown) {
  // @ts-expect-error: Object is of type 'unknown'.
  value.test('abc');

  assertIsRegExp(value);

  // %inferred-type: RegExp
  value;

  value.test('abc'); // OK
}

/** An assertion function */
function assertIsRegExp(arg: unknown): asserts arg is RegExp {
  if (! (arg instanceof RegExp)) {
    throw new TypeError('Not a RegExp: ' + arg);
  }
}

对于未知类型的参数,可以使用 assertIsRegExp函数来断言类型,在其内部如果传入的参数符合类型的话,就不会抛出错误。在 func下半部分编译器则收到了断言信号,就可以直接使用 test方法了。

这种写法跟 if/else的区别在于开发者写 if/else时需要显式地书写类型不在预期时的代码,否则不满足类型时可能缺少运行信息,导致正确类型条件下的代码不执行。

我们没法确保开发者一定会写类型不在预期时的处理逻辑,因此使用类型断言守卫也是一种可行之策,在运行时将会报错,如果前端项目运行在有效的监控体系下,也能收到错误信息以便于排查。

如果你喜欢这种类型守卫,可以使用elierotenberg/typed-assert: A typesafe TS assertion library这样的库来减少工作量,它帮我们写了 assertIsRegExp这样的函数。

如果你喜欢显式的 if/else条件判断,也可以使用mesqueeb/is-what: JS type check (TypeScript supported) functions like `isPlainObject() isArray()` etc. A simple & small integration.这样的超轻量类型检查库。

如果你不想引入其他库,那么只能自己写类型判断代码了,但是自己写的话可要注意了,来看看如下代码:

// 1)
typeof NaN === 'number' // true
// 🤔 ("not a number" is a "number"...)

// 2)
isNaN('1') // false
// 🤔 the string '1' is not-"not a number"... so it's a number??

// 3)
isNaN('one') // true
// 🤔 'one' is NaN but `NaN === 'one'` is false...

这只是一个 NaN特例,但这就是 JavaScript

好了,下次见。

参考

@AaronConlon AaronConlon added the 编程 软件开发的旅途 label Sep 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
编程 软件开发的旅途
Projects
None yet
Development

No branches or pull requests

1 participant