在大多数程序中,咱们必须依据输出做出决策。TypeScript 也不例外,应用条件类型能够形容输出类型与输入类型之间的关系。
本文同步首发在集体博客中,欢送订阅、交换。
用于条件判断时的 extends
当 extends 用于示意条件判断时,能够总结出以下法则
- 若位于 extends 两侧的类型雷同,则 extends 在语义上可了解为
===
,能够参考如下例子:
type result1 = 'a' extends 'abc' ? true : false // false
type result2 = 123 extends 1 ? true : false // false
- 若位于 extends 右侧的类型蕴含位于 extends 左侧的类型 (即狭隘类型 extends 宽泛类型) 时,后果为 true,反之为 false。能够参考如下例子:
type result3 = string extends string | number ? true : false // true
- 当 extends 作用于对象时,若在对象中指定的 key 越多,则其类型定义的范畴越狭隘。能够参考如下例子:
type result4 = {a: true, b: false} extends {a: true} ? true : false // true
在泛型类型中应用条件类型
思考如下 Demo 类型定义:
type Demo<T, U> = T extends U ? never : T
联合用于条件判断时的 extends,可知 'a' | 'b' | 'c' extends 'a'
是 false, 因而 Demo<'a' | 'b' | 'c', 'a'>
后果是 'a' | 'b' | 'c'
么?
查阅官网,其中有提到:
When conditional types act on a generic type, they become distributive when given a union type.
即当条件类型作用于泛型类型时,联结类型会被拆分应用。即 Demo<'a' | 'b' | 'c', 'a'>
会被拆分为 'a' extends 'a'
、'b' extends 'a'
、'c' extends 'a'
。用伪代码示意相似于:
function Demo(T, U) {
return T.map(val => {if (val !== U) return val
return 'never'
})
}
Demo(['a', 'b', 'c'], 'a') // ['never', 'b', 'c']
此外依据 never 类型的定义 —— never 类型可调配给每种类型,然而没有类型能够调配给 never(除了 never 自身)。即 never | 'b' | 'c'
等价于 'b' | 'c'
。
因而 Demo<'a' | 'b' | 'c', 'a'>
的后果并不是 'a' | 'b' | 'c'
而是 'b' | 'c'
。
工具类型
心细的读者可能曾经发现了 Demo 类型的申明过程其实就是 TypeScript 官网提供的工具类型中 Exclude<Type, ExcludedUnion>
的实现原理,其用于将联结类型 ExcludedUnion 排除在 Type 类型之外。
type T = Demo<'a' | 'b' | 'c', 'a'> // T: 'b' | 'c'
基于 Demo 类型定义,进一步地还能够实现官网工具类型中的 Omit<Type, Keys>
,其用于移除对象 Type
中满足 keys 类型的属性值。
type Omit<Type, Keys> = {[P in Demo<keyof Type, Keys>]: Type<P>
}
interface Todo {
title: string;
description: string;
completed: boolean;
}
type T = Omit<Todo, 'description'> // T: {title: string; completed: boolean}
逃离舱
如果想让 Demo<'a' | 'b' | 'c', 'a'>
的后果为 'a' | 'b' | 'c'
是否能够实现呢? 依据官网形容:
Typically, distributivity is the desired behavior. To avoid that behavior, you can surround each side of the extends keyword with square brackets.
如果不想遍历泛型中的每一个类型,能够用方括号将泛型给括起来以示意应用该泛型的整体局部。
type Demo<T, U> = [T] extends [U] ? never : T
// result 此时类型为 'a' | 'b' | 'c'
type result = Demo<'a' | 'b' | 'c', 'a'>
在箭头函数中应用条件类型
在箭头函数中应用三元表达式时,从左向右的浏览习惯导致函数内容区若不加括号则会让应用方感到困惑。比方下方代码中 x 是函数类型还是布尔类型呢?
// The intent is not clear.
var x = a => 1 ? true : false
在 eslint 规定 no-confusing-arrow 中,举荐如下写法:
var x = a => (1 ? true : false)
在 TypeScript 的类型定义中,若在箭头函数中应用 extends 也是同理,因为从左向右的浏览习惯,也会导致阅读者对类型代码的执行程序感到困惑。
type Curry<P extends any[], R> =
(arg: Head<P>) => HasTail<P> extends true ? Curry<Tail<P>, R> : R
因而在箭头函数中应用 extends 倡议加上括号,对于进行 code review 有很大的帮忙。
type Curry<P extends any[], R> =
(arg: Head<P>) => (HasTail<P> extends true ? Curry<Tail<P>, R> : R)
联合类型推导应用条件类型
在 TypeScript 中,个别会联合 extends 来应用类型推导 infer 语法。应用它能够实现主动推导类型的目标。比方用其来实现工具类型 ReturnType<Type>
,该工具类型用于返回函数 Type 的返回类型。
type ReturnType<T extends Function> = T extends (...args: any) => infer U ? U : never
MyReturnType<() => string> // string
MyReturnType<() => Promise<boolean> // Promise<boolean>
联合 extends 与类型推导还能够实现与数组相干的 Pop<T>
、Shift<T>
、Reverse<T>
工具类型。
Pop<T>
:
type Pop<T extends any[]> = T extends [...infer ExceptLast, any] ? ExceptLast : never
type T = Pop<[3, 2, 1]> // T: [3, 2]
Shift<T>
:
type Shift<T extends any[]> = T extends [infer _, ...infer O] ? O : never
type T = Shift<[3, 2, 1]> // T: [2, 1]
Reverse<T>
type Reverse<T> = T extends [infer F, ...infer Others]
? [...Reverse<Others>, F]
: []
type T = Reverse<['a', 'b']> // T: ['b', 'a']
应用条件类型来判断两个类型齐全相等
咱们也能够应用条件类型来判断 A、B 两个类型是否齐全相等。以后社区上次要有两种计划:
计划一: 参考 issue。
export type Equal1<T, S> =
[T] extends [S] ? ([S] extends [T] ? true : false
) : false
目前该计划的惟一毛病是会将 any 类型与其它任何类型判为相等。
type T = Equal1<{x:any}, {x:number}> // T: true
计划二: 参考 issue。
export type Equal2<X, Y> =
(<T>() => T extends X ? 1 : 2) extends
(<U>() => U extends Y ? 1 : 2) ? true : false
目前该计划的惟一毛病是在对穿插类型的解决上有一点瑕疵。
type T = Equal2<{x:1} & {y:2}, {x:1, y:2}> // false
以上两种判断类型相等的办法见仁见智,笔者在此抛砖引玉。