模模糊糊使用Ts进行开发一年多了,在这一年的使用中个人常常把Ts当作一门简单的工具作为使用。直到这段时间,挖掘了Ts类型编程更为深入的部分,才知道自己已经停滞不前了。
通过这篇文章,你将会
软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。
在TypeScript中泛型就承担着代替未来传入类型的责任,也可以把他看作一个能够作为类型参数变量。
下面来创建第一个使用泛型的例子:identity函数。 并且这个函数会返回任何传入它的值。
如果不用泛型的话,这个函数可能是下面这样:
function identity(arg: number): number {
return arg;
}
如上,如果使用基本变量的话。那么只能规定一种变量来作为值类型。
或者,我们使用any
类型来定义函数:
function identity(arg: any): any {
return arg;
}
使用any
类型会导致这个函数可以接收任何类型的arg
参数,但是这样就不符合我们的本意:传入的类型与返回的类型应该是相同的。
比如我们传入一个数字,我们只知道任何类型的值都有可能被返回,而不能确定这个值是不是数字。
因此,我们需要一种方法使返回值的类型与传入参数的类型是相同的。 这里,我们使用了 类型变量,它是一种特殊的变量,只用于表示类型而不是值。
function identity<T>(arg: T): T {
return arg;
}
我们给identity
函数添加了类型变量T
。 T
帮助我们捕获用户传入的类型(比如:number
),相当于给T这个变量赋值以后他就是一个常量值了。之后我们就可以使用这个类型,比如我们再次使用了 T
当做返回值类型。
通过使用变量T
,我们就成功的规定了入出类型值统一的函数。
在这个函数中,我们就变量T
,就称为了泛型。
我们定义了泛型函数后,可以用两种方法使用。 第一种是,传入所有的参数,包含类型参数:
let output = identity<string>("myString"); // type of output will be 'string'
这里我们明确的指定了T
是string
类型,并做为一个参数传给函数,使用了<>
括起来而不是()
。
第二种方法更普遍。利用了类型推论 – 即编译器会根据传入的参数自动地帮助我们确定T的类型:
let output = identity("myString"); // type of output will be 'string'
注意我们没必要使用尖括号(<>
)来明确地传入类型;编译器可以查看myString
的值,然后把T
设置为它的类型。 类型推论帮助我们保持代码精简和高可读性。如果编译器不能够自动地推断出类型的话,只能像上面那样明确的传入T的类型,在一些复杂的情况下,这是可能出现的。
接着identity泛型函数的例子,假设此时我们想打印出arg的长度:
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // Error: T doesn't have .length
return arg;
}
如果这么直接打印的话,犹豫我们此处定义的泛型T类型实际等于任意类型。编译器不能保证任意类型都具有length属性,例如number类型,所以就报错了。
现在假设我们想操作T
类型的数组而不直接是T
。由于我们操作的是数组,所以.length
属性是应该存在的。 我们可以像创建其它数组一样创建这个数组:
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
此处的T作为一个变量,当做类型的一部分使用,而不是整个类型,增加了灵活性。
在 loggingIdentity
例子中,我们想访问arg
的length
属性,但是编译器并不能证明每种类型都有length
属性,所以就报错了。
相比于操作any所有类型,我们想要限制函数去处理任意带有.length
属性的所有类型。 只要传入的类型有这个属性,我们就允许,就是说至少包含这一属性。 为此,我们需要列出对于T的约束要求。
为此,我们定义一个接口来描述约束条件。 创建一个包含 .length
属性的接口,使用这个接口和extends
关键字来实现约束:
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}
现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:
loggingIdentity(3); // Error, number doesn't have a .length property
我们需要传入符合约束类型的值,必须包含必须的属性:
loggingIdentity({length: 10, value: 3});
在泛型约束中使用类型参数
你可以声明一个类型参数,且它被另一个类型参数所约束。 比如,现在我们想要用属性名从对象里获取这个属性。 并且我们想要确保这个属性存在于对象 obj
上,因此我们需要在这两个类型之间使用约束。
function getProperty(obj: T, key: K) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.
高级类型意在提供组合式类型以及更明确所需类型范围,其中包含
交叉类型是将多个类型合并为一个类型的类型。这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。
现有Animal类型:
type Animal = {
name:string;
age:number;
}
需要创建一个新的类型Dog具有新的类型并具备Animal类型的属性,此时就用到了我们的交叉类型,符号为&。
type Dog = {
loveHuman:boolean
} & Animal
与运算符号&&类似,此时的Dog类型拥有了它特有的loveHuman属性也拥有了Animal中name以及age属性。
联合类型表示一个值可以是几种类型之一。 我们用竖线( |
)分隔每个类型,所以 number | string | boolean
表示一个值可以是 number
, string
,或 boolean
。
function setObject(key:string,value:string | number){
if(key == 'age'){
return value -2
}
if(key == 'name'){
return value
}
}
如上函数中我们通过联合类型表示函数所需的value参数为字符串或数字类型,对应设置了不同的返回。
如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。
interface Bird {
fly();
layEggs();
}
interface Fish {
swim();
layEggs();
}
function getSmallPet(): Fish | Bird {
// ...
}
let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim(); // errors
这里的联合类型可能有点复杂,但是你很容易就习惯了。 如果一个值的类型是 A | B
,我们能够 确定的是它包含了 A
和 B
中共有的成员。 这个例子里, Bird
具有一个 fly
成员。 我们不能确定一个 Bird | Fish
类型的变量是否有 fly
方法。 如果变量在运行时是 Fish
类型,那么调用 pet.fly()
就出错了。
联合类型适合于那些值可以为不同类型的情况。 但当我们想确切地了解是否为 Fish
时怎么办? JavaScript里常用来区分2个可能值的方法是检查成员是否存在。 如之前提及的,我们只能访问联合类型中共同拥有的成员。
let pet = getSmallPet();
// 每一个成员访问都会报错
if (pet.swim) {
pet.swim();
}
else if (pet.fly) {
pet.fly();
}
为了让这段代码工作,我们要使用类型断言:
let pet = getSmallPet();
if ((<Fish>pet).swim) {
(<Fish>pet).swim();
}
else {
(<Bird>pet).fly();
}
这里可以注意到我们不得不多次使用类型断言。 假若我们一旦检查过类型,就能在之后的每个分支里清楚地知道 pet
的类型的话就好了。
TypeScript里的 类型保护机制让它成为了现实。 类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。 要定义一个类型保护,我们只要简单地定义一个函数,它的返回值是一个 类型谓词:
function isFish(pet: Fish | Bird): pet is Fish {
return (<Fish>pet).swim !== undefined;
}
在这个例子里, pet is Fish
就是类型谓词。 谓词为 parameterName is Type
这种形式, parameterName
必须是来自于当前函数签名里的一个参数名。
每当使用一些变量调用 isFish
时,TypeScript会将变量缩减为那个具体的类型,只要这个类型与变量的原始类型是兼容的。
// 'swim' 和 'fly' 调用都没有问题了
if (isFish(pet)) {
pet.swim();
}
else {
pet.fly();
}
注意TypeScript不仅知道在 if
分支里 pet
是 Fish
类型; 它还清楚在 else
分支里,一定 不是 Fish
类型,一定是 Bird
类型。
其中用户可自定义的类型保护,除了使用is关键字外还有in 关键字。
使用 in 关键字,我们可以进一步收窄类型(Type Narrowing
),思考下面这个例子,要如何将 " A | B " 的联合类型缩小到"A"?
class A {
public a() {}
public useA() {
return "A";
}
}
class B {
public b() {}
public useB() {
return "B";
}
}
首先联想下 for...in
循环,它遍历对象的属性名,而 in
关键字也是一样,它能够判断一个属性是否为对象所拥有:
function useIt(arg: A | B): void {
'a' in arg ? arg.useA() : arg.useB();
}
如果参数中存在a
属性,由于A、B两个类型的交集并不包含a,所以这样能立刻收窄类型判断到 A 身上。
由于A、B两个类型的交集并不包含 a 这个属性,所以这里的 in
判断会精确地将类型对应收窄到三元表达式的前后。即 A 或者 B。
interface IBoy {
name: "mike";
gf: string;
}
interface IGirl {
name: "sofia";
bf: string;
}
function getLover(child: IBoy | IGirl): string {
if (child.name === "mike") {
return child.gf;
} else {
return child.bf;
}
}
关于字面量类型
literal types
,它是对类型的进一步限制,比如你的状态码只可能是 0/1/2,那么你就可以写成status: 0 | 1 | 2
的形式,而不是用一个number
来表达。
- 字符串字面量,常见如
mode: "dev" | "prod"
。- 布尔值字面量通常与其他字面量类型混用,如
open: true | "none" | "chrome"
。
在编程中遇到条件判断,我们常用 If 语句与三元表达式实现
if (condition) {
execute()
}
这种没有 else 的 If 语句,我也习惯写成:
condition ? execute() : void 0;
而 条件类型 的语法,实际上就是三元表达式,看一个最简单的例子:
T extends U ? X : Y
如果你觉得这里的 extends 不太好理解,可以暂时简单理解为 U 中的属性在 T 中都有。
为什么会有条件类型?可以看到 条件类型 通常是和 泛型 一同使用的,联想到泛型的使用场景以及值得延迟推断,我想你应该明白了些什么。对于类型无法即时确定的场景,使用 条件类型 来在运行时动态的确定最终的类型(运行时可能不太准确,或者可以理解为,你提供的函数被他人使用时,根据他人使用时传入的参数来动态确定需要被满足的类型约束)。
类比到编程语句中,其实就是根据条件判断来动态的赋予变量值:
let unknownVar: string;
unknownVar = condition ? "淘系前端" : "淘宝FED";
type LiteralType<T> = T extends string ? "foo" : "bar";
条件类型理解起来其实也很直观,唯一需要有一定理解成本的就是 何时条件类型系统会收集到足够的信息来确定类型,也就是说,条件类型有时不会立刻完成判断,比如工具库提供的函数,需要用户在使用时传入参数才会完成 条件类型 的判断。
本文通过泛型入手,循序渐进地讲解相关常用的进阶写法。
本文主要参考了:
Ts中文官网
TypeScript的另一面:类型编程(2021重制版)