TypeScript 类型

TypeScript 类型

neverunknown 类型分别在 TypeScript v2.0v3.0, 中引入。为了理解它们的用法,下面将深入讨论 TypeScript 类型。

本篇包含:

什么是 TypeScript 中的类型 ?

在学习 JavaScript 的过程中,我们发现,这门语言实在是太太太灵活了,它没有任何的类型约束,一个变量在声明为一个数值之后,又能重新赋值为一个字符串,因此,在有些时候,这给我们的编程带来了许多困扰:

let str = "我是一个字符串";    // 声明变量 str 为一个字符串
str = 123;    // 又给这个变量赋值一个数值
console.log(str.length)    // 报错,str 上不存在 length

在以上这个例子,str 这个变量名,让人误以为是一个字符串,调用字符串的 length 获取字符串长度,运行报错。而 TypeStript 的出现,就能够给 JavaScript 套上类型的“枷锁”,让它不那么灵活。在 TypeScript 中,类型是一组可能的值。例如 TypeScript 中的类型 string 是所有可能字符串的集合, number 是所有可能数值的集合。

除开这些基本类型以外,TypeScript 还具有联合和交集类型。像 string | number 这样的类型被称为“联合”类型,因为它实际上是所有字符串的集合和所有数字的集合的并集:

TypeScript string | number Set

该集合包含 stringnumber 集合 string | number 。因为包含所有number和所有 string 的值,所以 string | number 它被称为 stringnumber的 超类型。

unknown 是所有可能值的集合。任何值都可以分配为 unknown 类型。这意味着这是 unknown 所有其他类型的超类型。因此, unknown 称为顶级类型:

The Unknown Type In TypeScript

unknown 包含所有其他类型集合。never 是空集,当没有值的时候,就可以使用 never 类型。空集可以放入任何其他集合中,因此 never 是所有其他类型的子类型。这就是为什么 never 被称为底部类型:

The Never Type In TypeScript

unknown 所有其他类型的超类型,而 never 是空集,因此,我们能够得出下面的关系式:

T | neverT
T & unknownT
  • 别的类型对 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,而不是其他类型,比如voidundefined,因为这些类型表示函数可能会返回一个值,而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是一个空对象,上面不存在 cityfullname 属性, 因此,您需要在此处断言类型:

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 功能,如 typeofinstanceofin 等,在条件块的帮助下缩小类型范围。这些条件块可以是 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) => anyT 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,unknownany的区别

在以上的例子中, prettyPrint 函数中的 x,和 timeout 函数中的 promise 类型参数都是可以是任何类型的情况。不同之处在于,在 timeout中 ,promise 解析值可以简单地具有任何类型,因为它永远不会存在:

  • never : 在不会或不应该有值的情况下使用
  • unknown : 在有值,但不确定是什么类型的值的情况下使用
  • any : 当你真的不想使用类型检查的情况下才使用

总结

在一般情况下,都使用范围最小,最具体的类型,来进行约束。never 是最具体的类型,因为没有小于空集的集合。 unknown 是最不具体的类型,因为它包含所有可能的值。 any 不是一个集合,它会破坏类型检查,所以尽量不要使用它。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值