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
。让我们逐步分析:
- 类型参数:
T
是任意类型K
是任意键的联合类型(keyof any
表示任何可能的键类型)
- 映射类型的工作原理:
[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 类型体操也是越跳越有趣。