- Published on
第二十七章 TypeScript 类型缩小
- Authors
- Name
- Tian Haipeng
手动类型缩小
如果一个变量属于联合类型,所以使用时一般需要缩小类型。
第一种方法是使用if
判断。
function getScore(value: number | string): number {
if (typeof value === 'number') {
// (A)
// %inferred-type: number
value
return value
}
if (typeof value === 'string') {
// (B)
// %inferred-type: string
value
return value.length
}
throw new Error('Unsupported value: ' + value)
}
如果一个值是any
或unknown
,你又想对它进行处理,就必须先缩小类型。
function parseStringLiteral(stringLiteral: string): string {
const result: unknown = JSON.parse(stringLiteral)
if (typeof result === 'string') {
// (A)
return result
}
throw new Error('Not a string literal: ' + stringLiteral)
}
下面是另一个例子。
interface Book {
title: null | string
isbn: string
}
function getTitle(book: Book) {
if (book.title === null) {
// %inferred-type: null
book.title
return '(Untitled)'
} else {
// %inferred-type: string
book.title
return book.title
}
}
缩小类型的前提是,需要先获取类型。获取类型的几种方法如下。
function func(value: Function | Date | number[]) {
if (typeof value === 'function') {
// %inferred-type: Function
value
}
if (value instanceof Date) {
// %inferred-type: Date
value
}
if (Array.isArray(value)) {
// %inferred-type: number[]
value
}
}
typeof 运算符
第二种方法是使用switch
缩小类型。
function getScore(value: number | string): number {
switch (typeof value) {
case 'number':
// %inferred-type: number
value
return value
case 'string':
// %inferred-type: string
value
return value.length
default:
throw new Error('Unsupported value: ' + value)
}
}
instanceof 运算符
第三种方法是 instanceof 运算符。它能够检测实例对象与构造函数之间的关系。instanceof 运算符的左操作数为实例对象,右操作数为构造函数,若构造函数的 prototype 属性值存在于实例对象的原型链上,则返回 true;否则,返回 false。
function f(x: Date | RegExp) {
if (x instanceof Date) {
x // Date
}
if (x instanceof RegExp) {
x // RegExp
}
}
instanceof 类型守卫同样适用于自定义构造函数,并对其实例对象进行类型细化。
class A {}
class B {}
function f(x: A | B) {
if (x instanceof A) {
x // A
}
if (x instanceof B) {
x // B
}
}
in 运算符
第四种方法是使用 in 运算符。
in 运算符是 JavaScript 中的关系运算符之一,用来判断对象自身或其原型链中是否存在给定的属性,若存在则返回 true,否则返回 false。in 运算符有两个操作数,左操作数为待测试的属性名,右操作数为测试对象。
in 类型守卫根据 in 运算符的测试结果,将右操作数的类型细化为具体的对象类型。
interface A {
x: number
}
interface B {
y: string
}
function f(x: A | B) {
if ('x' in x) {
x // A
} else {
x // B
}
}
interface A {
a: number
}
interface B {
b: number
}
function pickAB(ab: A | B) {
if ('a' in ab) {
ab // Type is A
} else {
ab // Type is B
}
ab // Type is A | B
}
缩小对象的属性,要用in
运算符。
type FirstOrSecond = { first: string } | { second: string }
function func(firstOrSecond: FirstOrSecond) {
if ('second' in firstOrSecond) {
// %inferred-type: { second: string; }
firstOrSecond
}
}
// 错误
function func(firstOrSecond: FirstOrSecond) {
// @ts-expect-error: Property 'second' does not exist on
// type 'FirstOrSecond'. [...]
if (firstOrSecond.second !== undefined) {
// ···
}
}
in
运算符只能用于联合类型,不能用于检查一个属性是否存在。
function func(obj: object) {
if ('name' in obj) {
// %inferred-type: object
obj
// 报错
obj.name
}
}
特征属性
对于不同对象之间的区分,还可以人为地为每一类对象设置一个特征属性。
interface UploadEvent {
type: 'upload'
filename: string
contents: string
}
interface DownloadEvent {
type: 'download'
filename: string
}
type AppEvent = UploadEvent | DownloadEvent
function handleEvent(e: AppEvent) {
switch (e.type) {
case 'download':
e // Type is DownloadEvent
break
case 'upload':
e // Type is UploadEvent
break
}
}
any 类型的细化
TypeScript 推断变量类型时,会根据获知的信息,不断改变推断出来的类型,越来越细化。这种现象在any
身上特别明显。
function range(start: number, limit: number) {
const out = [] // 类型为 any[]
for (let i = start; i < limit; i++) {
out.push(i)
}
return out // 类型为 number[]
}
上面示例中,变量out
的类型一开始推断为any[]
,后来在里面放入数值,类型就变为number[]
。
再看下面的例子。
const result = [] // 类型为 any[]
result.push('a')
result // 类型为 string[]
result.push(1)
result // 类型为 (string | number)[]
上面示例中,数组result
随着成员类型的不同,而不断改变自己的类型。
注意,这种any
类型的细化,只在打开了编译选项noImplicitAny
时发生。
这时,如果在变量的推断类型还为any
时(即没有任何写操作),就去输出(或读取)该变量,则会报错,因为这时推断还没有完成,无法满足noImplicitAny
的要求。
const result = [] // 类型为 any[]
console.log(typeof result) // 报错
result.push('a') // 类型为 string[]
上面示例中,只有运行完第三行,result
的类型才能完成第一次推断,所以第二行读取result
就会报错。
is 运算符
is
运算符返回一个布尔值,用来判断左侧的值是否属于右侧的类型。
function isInputElement(el: HTMLElement): el is HTMLInputElement {
return 'value' in el
}
function getElementContent(el: HTMLElement) {
if (isInputElement(el)) {
el // Type is HTMLInputElement
return el.value
}
el // Type is HTMLElement
return el.textContent
}
function isDefined<T>(x: T | undefined): x is T {
return x !== undefined
}