从 Vue3 源码中了解你所不知道的 never

never 翻译为从不,从未,绝不。在 TypeScript 中,never 类型表示的是那些永远不会发生的值的类型。

平时在工作中可能 never 使用的并不多,但实际上,never 在 TypeScript 中扮演着重要角色,它是任何类型的子类型,但没有类型是 never 的子类型(除了 never 本身)。

在 Vue3 源码中就有大量 never的使用,接下来我们就通过 Vue3 源码来详细了解 never 在 TypeScript 几种用法。

不可能有返回值的函数

首先是最常见的场景,表示不可能有返回值的函数的返回类型:

ts 复制代码
// https://github1s.com/vuejs/core/blob/main/packages/compiler-sfc/src/script/context.ts#L153-L157
error(msg: string, node: Node, scope?: TypeScope): never {
  throw new Error(
    `[@vue/compiler-sfc] ${generateError(msg, node, this, scope)}`,
  )
}
// https://github1s.com/vuejs/core/blob/main/packages/runtime-test/src/nodeOps.ts#L231-L233
function querySelector(): never {
  throw new Error('querySelector not supported in test renderer.')
}

当一个函数永远不会有返回值(比如抛出异常或无限循环),其返回类型可以标注为 never

在上面两个场景中,函数的作用都是抛出一个异常,所以不存在返回任何值的情况,我们就可以把返回类型设为 never,除此之外,一个无限循环的函数,返回值也同样为 never

typescript 复制代码
function infiniteLoop(): never {
  while (true) {}
}

void 的区别是,void 是没有返回值,而 never 根本走不到返回那一步。

类型保护中的穷尽检查(Exhaustiveness checking)

穷尽检查(Exhaustiveness checking),是指在使用联合类型和类型保护时,never 可以帮助我们确保所有情况都被处理到了。如果有遗漏,TypeScript 就会报错。

下面是 Vue3 中的一段源码:

ts 复制代码
// https://github1s.com/vuejs/core/blob/main/packages/compiler-ssr/src/ssrCodegenTransform.ts#L169-L192
switch (child.tagType) {
  case ElementTypes.ELEMENT:
    ssrProcessElement(child, context)
    break
  case ElementTypes.COMPONENT:
    ssrProcessComponent(child, context, parent)
    break
  case ElementTypes.SLOT:
    ssrProcessSlotOutlet(child, context)
    break
  case ElementTypes.TEMPLATE:
    // TODO
    break
  default:
    context.onError(
      createSSRCompilerError(
        SSRErrorCodes.X_SSR_INVALID_AST_NODE,
        (child as any).loc,
      ),
    )
    // make sure we exhaust all possible types
    const exhaustiveCheck: never = child
    return exhaustiveCheck
}

可以看到在这个例子中,为了确保 child.tagType 的所有类型情况都被处理,在 default 中使用了 never 类型,这样如果遇到了 ELEMENTCOMPONENTSLOTTEMPLATE 之外的类型,TypeScript 就会在给 never 赋值时报错,提醒你补全分支。

这种方式可以防止出现后续增加类型而忘记在此处兼容的情况。

类似于 switch 在使用 if/else 时,也可以通过这种方式来确保某个变量的所有类型都被处理。

typescript 复制代码
function processValue(value: string | number) {
  if (typeof value === 'string') {
    // 处理字符串
  } else if (typeof value === 'number') {
    // 处理数字
  } else {
    // 这里 value 的类型是 never
    const impossible: never = value;
    // 如果以后 value 类型扩展了,这里会报错提醒我们处理新类型
  }
}

接口或类型中的不允许存在的属性

有时我们希望某个类型绝对不能有某些属性,可以用 never 来限制。

ts 复制代码
// https://github1s.com/vuejs/core/blob/main/packages/runtime-core/src/apiSetupHelpers.ts#L191-L231
export function defineOptions<
  RawBindings = {},
  D = {},
  C extends ComputedOptions = {},
  M extends MethodOptions = {},
  Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
  Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
>(
  options?: ComponentOptionsBase<
    {},
    RawBindings,
    D,
    C,
    M,
    Mixin,
    Extends,
    {}
  > & {
    /**
     * props should be defined via defineProps().
     */
    props?: never
    /**
     * emits should be defined via defineEmits().
     */
    emits?: never
    /**
     * expose should be defined via defineExpose().
     */
    expose?: never
    /**
     * slots should be defined via defineSlots().
     */
    slots?: never
  },
): void {
  if (__DEV__) {
    warnRuntimeUsage(`defineOptions`)
  }
}

defineOptions 函数中,为了防止在 options 中使用 propsemits 等属性,Vue3 把这些属性全部定义为了 never,这样当它包含这些属性时就会报错。

类型约束

可以通过 never 限制数组元素类型,确保数组永远为空。

ts 复制代码
// https://github1s.com/vuejs/core/blob/main/packages/shared/src/general.ts#L6
export const EMPTY_ARR: readonly never[] = __DEV__ ? Object.freeze([]) : []

此处 EMPTY_ARR 只能是一个空数组,不能往里面添加任何元素,哪怕是 push(undefined) 也会报类型错误。

这是一种极致安全的空数组声明方式,常用于全局复用的空数组常量,防止被意外修改或误用。

在条件类型中使用

在 Vue3 还用了很多 never,而比起上面的简单场景,剩下的场景就显得有些复杂。

限制类型

ts 复制代码
// https://github1s.com/vuejs/core/blob/main/packages/compiler-sfc/src/script/resolveType.ts#L654
type GetSetType<T> = T extends Set<infer V> ? V : never

这段代码表示:如果 TSet 类型,则提取出 Set 里的元素类型 V,否则返回 never

而返回 never 表示没有类型,在此处也意味着没有这种情况,那就是说,GetSetType 只接受 Set 类型,当我们传入其他类型,就会报错。

这是一种典型的通过 never 来限制类型的应用场景,在类型工具库中非常常见。

过滤类型

接下来看一个更复杂的:

ts 复制代码
// https://github1s.com/vuejs/core/blob/main/packages/runtime-core/src/apiSetupHelpers.ts#L100-L104
type BooleanKey<T, K extends keyof T = keyof T> = K extends any
  ? [T[K]] extends [boolean | undefined]
    ? K
    : never
  : never
  • 首先 T 是任意类型,而 KT 的键类型,默认值为 keyof T(即 T 的所有键的联合类型)。
  • 其次,K extends any 是一个分布式的条件类型,会对 K 中的每个类型分别进行判断 (关于分布式的条件类型,可以看 这篇文章)。
  • [T[K]] extends [boolean | undefined] 检查 T[K] 的类型是否为 booleanundefined,如果条件为真,返回 K,否则返回 never
  • 返回 never 意味着不存在,也就是说,这个类型只提取出 K 中对应 T[K]booleanundefined 的值。

我知道这段表述比较抽象,所以简单举个例子:

ts 复制代码
interface Props {
  name: string;
  isActive: boolean;
  isVisible?: boolean;
  count: number;
}

type BooleanProps = BooleanKey<Props>; // 结果: "isActive" | "isVisible"

解释:TProps,则 K"name" | "isActive" | "isVisible" | "count"。而 T[K] 中只有 "isActive" | "isVisible" 属性值的类型符合 boolean | undefined,所以 BooleanKey<Props> 会返回 "isActive" | "isVisible"

删除指定属性

这也是一个典型的用法。

ts 复制代码
// https://github1s.com/vuejs/core/blob/main/packages/runtime-core/src/apiSetupHelpers.ts#L314-L316
type MappedOmit<T, K extends keyof any> = {
  [P in keyof T as P extends K ? never : P]: T[P]
}

这是一个工具类型,用于从类型 T 中排除指定的键 K。让我们逐步分析:

  1. 类型参数:
  • T 是任意类型
  • K 是任意键的联合类型(keyof any 表示任何可能的键类型)
  1. 映射类型的工作原理:
  • [P in keyof T] 遍历 T 的所有键
  • as P extends K ? never : P 是一个键的重映射:如果键 PK 中,则映射为 never(也就是删除该键),如果键 P 不在 K 中,则保持原键名。

还是举个例子说明:

ts 复制代码
interface User {
  id: number;
  name: string;
  age: number;
  email: string;
}

// 排除 'id' 和 'email' 字段
type UserWithoutIdAndEmail = MappedOmit<User, 'id' | 'email'>;

// 结果类型等价于:
interface UserWithoutIdAndEmail {
  name: string;
  age: number;
}

类型守卫

比起上面的场景,下面这个最为复杂:

ts 复制代码
type InferPropType<T, NullAsAny = true> = [T] extends [null]
  ? NullAsAny extends true
    ? any
    : null
  : [T] extends [{ type: null | true }]
    ? any // As TS issue https://github.com/Microsoft/TypeScript/issues/14829 // somehow `ObjectConstructor` when inferred from { (): T } becomes `any` // `BooleanConstructor` when inferred from PropConstructor(with PropMethod) becomes `Boolean`
    : [T] extends [ObjectConstructor | { type: ObjectConstructor }]
      ? Record<string, any>
      : [T] extends [BooleanConstructor | { type: BooleanConstructor }]
        ? boolean
        : [T] extends [DateConstructor | { type: DateConstructor }]
          ? Date
          : [T] extends [(infer U)[] | { type: (infer U)[] }]
            ? U extends DateConstructor
              ? Date | InferPropType<U, false>
              : InferPropType<U, false>
            : [T] extends [Prop<infer V, infer D>]
              ? unknown extends V
                ? keyof V extends never
                  ? IfAny<V, V, D>
                  : V
                : V
              : T

不过别怕,我们只关注 never 部分:

keyof V extends never ? IfAny<V, V, D> : V

在这段代码中,keyof V extends never 是一个类型守卫,用于检查类型 V 是否是一个空对象类型。具体来说:

  • keyof V 获取类型 V 的所有键的联合类型
  • extends never 检查这个联合类型是否为空(即 never 类型)

所以我们可以通过 keyof V extends never 来检查一个类型 V 是否为空对象类型。

ts 复制代码
type Empty = {}
type Result1 = keyof Empty extends never ? true : false  // true

type WithProps = { name: string }
type Result2 = keyof WithProps extends never ? true : false  // false

总结

除了上述几种情况,不知道 never 是否还有其他用法。但其实我在各种文档中都没有看到 never 用法的全部介绍。比起语法层面,我更倾向于上述这些用法,更类似于理解之后的融会贯通地灵活应用。

TypeScript 越研究越发现确实还是有很多隐藏剧情,TypeScript 类型体操也是越跳越有趣。

相关推荐
dualven_in_csdn1 小时前
搞了两天的win7批处理脚本问题
java·linux·前端
你的人类朋友2 小时前
✍️【Node.js程序员】的数据库【索引优化】指南
前端·javascript·后端
小超爱编程3 小时前
纯前端做图片压缩
开发语言·前端·javascript
应巅3 小时前
echarts 数据大屏(无UI设计 极简洁版)
前端·ui·echarts
Jimmy3 小时前
CSS 实现描边文字效果
前端·css·html
islandzzzz4 小时前
HMTL+CSS+JS-新手小白循序渐进案例入门
前端·javascript·css·html
Senar4 小时前
网页中如何判断用户是否处于闲置状态
前端·javascript
很甜的西瓜4 小时前
typescript软渲染实现类似canvas的2d矢量图形引擎
前端·javascript·typescript·图形渲染·canvas
Allen Bright4 小时前
【CSS-9】深入理解CSS字体图标:原理、优势与最佳实践
前端·css
阿芯爱编程5 小时前
最长和谐子序列,滑动窗口
前端·javascript·面试