TypeScript 类型
never
和 unknown
类型分别在 TypeScript v2.0 和 v3.0, 中引入。为了理解它们的用法,下面将深入讨论 TypeScript 类型。
本篇包含:
- 什么是 TypeScript 中的类型 ?
- TypeScript 中的
never
- 类型推断和类型注释
- 什么是类型断言 ?
- 什么是类型守卫 ?
- 与条件类型一起使用
never
- 何时使用
unknow
- 什么是
unknow
的类型收窄 ? - TypeScript 中,
never
,unknow
和any
的区别
什么是 TypeScript 中的类型 ?
在学习 JavaScript 的过程中,我们发现,这门语言实在是太太太灵活了,它没有任何的类型约束,一个变量在声明为一个数值之后,又能重新赋值为一个字符串,因此,在有些时候,这给我们的编程带来了许多困扰:
let str = "我是一个字符串"; // 声明变量 str 为一个字符串
str = 123; // 又给这个变量赋值一个数值
console.log(str.length) // 报错,str 上不存在 length
在以上这个例子,str
这个变量名,让人误以为是一个字符串,调用字符串的 length 获取字符串长度,运行报错。而 TypeStript 的出现,就能够给 JavaScript 套上类型的“枷锁”,让它不那么灵活。在 TypeScript 中,类型是一组可能的值。例如 TypeScript 中的类型 string
是所有可能字符串的集合, number
是所有可能数值的集合。
除开这些基本类型以外,TypeScript 还具有联合和交集类型。像 string | number
这样的类型被称为“联合”类型,因为它实际上是所有字符串的集合和所有数字的集合的并集:
该集合包含 string
和 number
集合 string | number
。因为包含所有number
和所有 string
的值,所以 string | number
它被称为 string
和 number
的 超类型。
unknown
是所有可能值的集合。任何值都可以分配为 unknown
类型。这意味着这是 unknown
所有其他类型的超类型。因此, unknown
称为顶级类型:
unknown
包含所有其他类型集合。never
是空集,当没有值的时候,就可以使用 never
类型。空集可以放入任何其他集合中,因此 never
是所有其他类型的子类型。这就是为什么 never
被称为底部类型:
unknown
所有其他类型的超类型,而 never
是空集,因此,我们能够得出下面的关系式:
T | never ⇒ T
T & unknown ⇒ T
- 别的类型对
never
联合(并集),结果还是原类型; - 别的类型对
unknown
交叉(交集),结果当然是最大的unknown
TypeScript 中的 never
下面是一个网络请求超时的函数:
function timeout(ms: number): Promise<never> {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout elapsed")), ms)
)
}
在以上代码中,函数timeout
返回一个Promise,这个Promise永远不会被resolved,只会被 rejected,因为它是一个超时Promise,表示在指定时间内没有得到结果,就会抛出一个超时错误。
因为这个 promise永远不会被 resolved,所以它的返回类型应该是never
,而不是其他类型,比如void
或undefined
,因为这些类型表示函数可能会返回一个值,而never
表示函数永远不会返回任何值。虽然,我们可以对 promise 类型参数使用任何类型,都不会报错,但最具体的(范围最小的)类型是 never
。因此,函数timeout
的返回类型应该是never
。
当我们通过 promise 请求一个 {peice:number} 类型的时候,为了考虑请求超时的情况,在其中使用函数timeout
,就是说,该请求 resolved 的时候,返回类型是 {peice:number},当 请求 rejected 的时候,返回类型是 never
,即 { price: number } | never
,由上方关系式可得: { price: number }
,因此,这个请求函数的返回类型就可以写成 { price: number }
。
但 ,如果函数timeout
返回类型写别的类型,在函数timeout
中虽然不会报错,但最外层请求函数的返回类型就不会如此简单,如果使用 any
类型,则会失去类型检查的好处。
类型推断和类型注释
TypeScript 的核心是一个类型检查系统,当变量类型不在指定的类型内,就会类型引发报错。
在以下示例中,我们对每个变量都强制指定了数据类型,当赋值类型与指定类型冲突的时候,就会引发报错,如变量 city
let age: number = 9;
let name: string = "Allison";
let isSubscribed: boolean = true;
let city: string = 202; // 类型“number”不能分配给类型“string”
这种分配类型的行为称为类型批注。但是在某些情况下,TypeScript 本身足够聪明,可以自动识别数据类型。例如,在下面的情况下,TypeScript 就自动为 name
分配了一个 “string” 数据类型。
let name = 'Allison' // name is `number` type
同样,TypeScript 也可以从 return
语句中自动“推断”类型。在以下示例中,该 add
函数的返回类型为 number
。这种分配和“推断”类型的方式称为类型推导。当然,您也可以手动指定类型,但最好是让 TypeScript 自动推断它们:
function add(a:number, b:number){
return a + b
}
什么是类型断言?
当你认为自动推导的类型有问题,或者你更想要自己指定一个合适的类型的时候,可以通过 as
,自己指定类型,这被称为类型断言。
在下面的示例中,TypeScript 无法分配合适类型,因为它认为 Person
是一个空对象,上面不存在 city
和 fullname
属性, 因此,您需要在此处断言类型:
var Person = {};
Person.city = 'Kyoto'; // 错误: `{}`上不存在`city`
Person.fullname = 'Allison'; // 错误: `{}`上不存在`fullname`
在这里,我们可以通过interface
创建一个类型对象IPerson
,再使用 as
关键字将 Person
断言成 IPerson
:
interface IPerson {
city: string;
fullname: string;
}
var Person = {} as IPerson
Person.city = 'Kyoto';
Person.fullname = 'Allison';
接口 IPerson {
城市:字符串;
fullname:字符串;
}
var Person = {} 作为 IPerson
Person.city = '京都';
Person.fullname = '艾莉森';
这样,Person
对象就通过类型断言,有了一个合适的类型,并且也保持了类型检查的能力。
什么是类型守卫?
类型守卫是一种技巧,它使用原生 JavaScript 功能,如 typeof
、 instanceof
、 in
等,在条件块的帮助下缩小类型范围。这些条件块可以是 if/else 或 switch 语句。
例如,在以下示例中,可以使用类型守卫,通过 typeof
来判断提供给 test
函数的值 n
是否为字符串类型,然后输出 n.length
。否则,执行 console.log("n is a number")
。这就是使用 if/else
条件块进行保护守卫,并根据收窄后的类型,执行不同任务的方法:
function test(n: string | number){
if(typeof n === string){
console.log(n.length)
}
else {
console.log("n is a number")
}
}
这是另一个例子,其中利用 instanceof
方法,将 say
函数传入参数的类型 { Person | Animal }
进行收窄:
class Person {
say : "我是一个 Person!"
}
class Dog {
say : "我是一只 Dog!"
}
function (arg : Person | Dog){
if(arg instanceof Person){
console.log(`Person say: ${arg.height}`)
}
if(arg instanceof Dog){
console.log(`Dog say: ${arg.height}}`)
}
}
say(new Person()); // 输出:"Person say: 我是一个 Person!"
say(new Dog()); // 输出:"Dog say: 我是一只 Dog!"
与条件类型一起使用 never
在TypeScript中,infer
关键字用于在条件类型(conditional types)中引入类型变量,并从待推断的类型中提取出特定的类型。它通常与extends
关键字一起使用,用于提取泛型类型中的类型参数。
在条件类型中,我们经常能够看到 never
,例如:
type Arguments<T> = T extends (...args: infer A) => any ? A : never
type Return<T> = T extends (...args: any[]) => infer R ? R : never
function time<F extends Function>
(fn: F, ...args: Arguments<F>): Return<F> {
console.time()
const result = fn(...args)
console.timeEnd()
return result
}
让我来解释以上代码
type Arguments<T> = T extends (...args: infer A) => any ? A : never
这一行定义了一个条件类型Arguments
,它接受一个类型参数T
。它使用extends
关键字来检查T
是否为一个函数类型,如果是的话,它通过infer
关键字提取函数的参数类型,并将其赋值给类型变量A
。如果T
不是一个函数类型,那么返回never
类型。这样,Arguments<T>
类型将提取出函数T
的参数类型。
type Return<T> = T extends (...args: any[]) => infer R ? R : never
这一行定义了另一个条件类型Return
,它也接受一个类型参数T
。它同样使用extends
关键字来检查T
是否为一个函数类型,如果是的话,它通过infer
关键字提取函数的返回值类型,并将其赋值给类型变量R
。如果T
不是一个函数类型,那么返回never
类型。这样,Return<T>
类型将提取出函数T
的返回值类型。
function time<F extends Function>(fn: F, ...args: Arguments<F>): Return<F>
这个函数time
接受一个泛型类型F
,它是一个函数类型。在参数列表中,它接受一个函数fn
和一系列参数。使用Arguments<F>
来提取函数F
的参数类型,并使用Return<F>
来提取函数F
的返回值类型。
在这个函数体内部,它首先记录时间,然后调用传入的函数fn
,并记录函数执行结束后的时间。最后,它返回函数fn
的执行结果。
在这个示例中,infer
关键字的作用是从待推断的类型中提取出特定的类型,使得我们能够在条件类型中使用这个类型。这样可以让我们在函数time
中使用泛型类型F
的参数类型和返回值类型,而不需要显式地指定它们。
以上的例子中,在Arguments<T>
和Return<T>
中,如果T
不是一个函数类型,或者在time
函数中,如果我们在函数体内部使用了throw
语句抛出异常,又或者是在一个无限循环中,这样的话函数time
就不会正常返回,因此我们将其返回类型标注为never
。那么条件T extends (...args: infer A) => any
或T extends (...args: any[]) => infer R
将会被判定为false
,这时就会返回never
类型,从而使得类型系统更加严谨,提高代码的安全性和可靠性。
何时使用类型 unknown
任何值都可以分配 unknown
类型。因此,当值可能具有任何类型时,或者当使用更具体的类型不方便时,请使用 unknown
。例如,一个完美的打印函数应该能够接受任何类型的值并打印出来:
function prettyPrint(x: unknown): string {
if (Array.isArray(x)) {
return "[" + x.map(prettyPrint).join(", ") + "]"
}
if (typeof x === "string") {
return `"${x}"`
}
if (typeof x === "number") {
return String(x)
}
return "etc."
}
你不能直接调用一个 unknown
类型的值的属性或方法,但是,可以使用类型守卫来缩小类型范围,并对在缩小类型上运行的代码块进行准确的类型检查。
在 TypeScript 3.0 之前,最好的编写 prettyPrint
函数的方式是使用 any
,类型收窄后同样可以让编译器检查是否正确使用 map
方法,但是,如果误以为已经收窄类型,any
并不会报错,而 unknown
会提示你。
什么是 unknown
的类型收窄?
顾名思义,类型收窄用于收窄使用 unknown
声明的类型。当使用 unknown
声明值时,TypeScript 不知道它的类型,这意味着你不能使用它的任何方法,直到你显式地将其类型“缩小”到特定的类型。
例如,我们知道该 length
属性在 string
数据类型上可用。我们可以使用以下命令得到字符串的长度:
"hello".length // 5
现在,如果将其分配给 unknown
类型,则以下代码将引发错误,因为它不确定 unknown
类型上是否存在.length
属性:
function checkLength(value : unknown){
console.log(value.length) // 报错
}
checkLength('hello')
要解决此问题,只需要缩小 value
类型范围并检查其类型是否为 string
:
function checkLength(value: unknown){
if(typeof value === 'string'){
console.log(value.length) // 5
}
}
checkLength('hello')
TypeScript 中,never
,unknown
和any
的区别
在以上的例子中, prettyPrint
函数中的 x
,和 timeout
函数中的 promise 类型参数都是可以是任何类型的情况。不同之处在于,在 timeout
中 ,promise 解析值可以简单地具有任何类型,因为它永远不会存在:
never
: 在不会或不应该有值的情况下使用unknown
: 在有值,但不确定是什么类型的值的情况下使用any
: 当你真的不想使用类型检查的情况下才使用
总结
在一般情况下,都使用范围最小,最具体的类型,来进行约束。never
是最具体的类型,因为没有小于空集的集合。 unknown
是最不具体的类型,因为它包含所有可能的值。 any
不是一个集合,它会破坏类型检查,所以尽量不要使用它。