从 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 类型,这样如果遇到了 ELEMENT, COMPONENT, SLOT, TEMPLATE 之外的类型,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 中使用 props、emits 等属性,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

这段代码表示:如果 T 是 Set 类型,则提取出 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 是任意类型,而 K 是 T 的键类型,默认值为 keyof T(即 T 的所有键的联合类型)。
  • 其次,K extends any 是一个分布式的条件类型,会对 K 中的每个类型分别进行判断 (关于分布式的条件类型,可以看 这篇文章)。
  • [T[K]] extends [boolean | undefined] 检查 T[K] 的类型是否为 boolean 或 undefined,如果条件为真,返回 K,否则返回 never。
  • 返回 never 意味着不存在,也就是说,这个类型只提取出 K 中对应 T[K] 为 boolean 或 undefined 的值。

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

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

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

解释:T 为 Props,则 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 是一个键的重映射:如果键 P 在 K 中,则映射为 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 类型体操也是越跳越有趣。

相关推荐
lovingsoft5 分钟前
Chrome Lighthouse 详解
前端·chrome
Qlly15 分钟前
全栈工程师怎么接需求?从一句「加个功能」聊到能开发、能验收
前端·后端
马可家的菠萝19 分钟前
你的微信“文件传输助手”,是不是已经成了第二个网盘?
前端·后端·架构
略略略啦20 分钟前
Vibe Coding 基于Spec DD和TDD的实践指南
前端
shmily麻瓜小菜鸡41 分钟前
前端实现图片与PDF文字识别敏感信息打码方案
前端·pdf·状态模式
打工仔折腾 AI1 小时前
LangChain 工具调用实战:从 Tool 定义到 Agent 执行链路拆解
java·前端·人工智能·后端·python·langchain·ai agent 实战
汉堡大王95271 小时前
单次输出拉到100万Token:拆完 Gemini 4 Argon 的续写协议,才看懂谷歌这7个月在憋什么
前端·人工智能·后端
看浪的路人1 小时前
第9讲:大模型应用监控与可观测性——OpenTelemetry + Prometheus + Grafana 全链路追踪
java·前端·spring
内存过载1 小时前
用表格模拟甘特图:项目排期的土办法
前端·低代码
葡萄城技术团队1 小时前
用 SpreadJS 数据验证实现省市区三级联动
前端