接下来我来讲 TS 的高级类型,所谓高级类型就是指 TS 为了语言的灵活性所引入的一些语言特性,这些特性将帮助我们应对复杂多变的开发场景。本篇文章就来讲 TS 的交叉类型和索引类型。
交叉类型将多个类型合并为一个类型,新的类型具有所有类型的特性,所以交叉类型特别适合对象混入(mixin)的场景。
interface Person {
run(): void;
}
interface Teacher {
goto(): void;
}
let active: Person & Teacher = {
run() {},
goto() {}
};
这里我们定义了两个接口 Person 和 Teacher,接口 Person 具有一个方法 run,接口 Teacher 具有一个方法 goto。我们又定义了一个变量 active,它的类型为接口 Person 和 Teacher 的交叉类型,它同时具有方法 run 和 goto。
交叉类型同 "&" 进行连接,此时的变量 active 就应该具备两个接口类型所拥有的成员方法。这里需要注意的是,虽然从名称上看交叉类型给人的感觉是类型的交替,但实际上它是取所有类型的并集。
接下来我们再来看几种和交叉类型相关的类型。
联合类型让一个变量可以是多个类型,具有其中一个类型的特性。
let n: string | number = 1;
n = 'abc';
这里我们定义了一个变量 n,它的类型为 string 和 number 的联合类型,可以将 1 和 'abc' 都赋值给它。
联合类型通 "|" 进行连接,此时的变量 n 可以具有 string 和 number 其中一个类型的特性。
有时候我们不仅要限定一个变量的类型,还要限定这个变量的取值在某个特定范围之内,就可以使用字面量类型。
let m: 'm' | 2;
m = 'm';
m = 2;
m = 3; // 报错
这里我们定义了一个变量 m,它的类型是字面量类型的联合类型,表示 m 的取值只能是 'm' 或 2。如果我们给 m 赋值3,就会报错:Type '3' is not assignable to type '"m" | 2'.。
回到上篇文章中的例子,我们给这两个接口分别都重写了 toString 方法。
enum Type { obj, arr }
class IsObject {
toObject() {
console.log('hello object');
}
toString() {
console.log('hello toString');
}
}
class IsArray {
toArray() {
console.log('hello array');
}
toString() {
console.log('hello toString');
}
}
function getType(type: Type) {
let target = type === Type.obj ? new IsObject() : new IsArray(); // 联合类型
return target;
}
getType(Type.obj);
这里的 target 就是一个 IsObject 和 IsArray 的联合类型。
在这里需要提一下,如果一个变量是一个联合类型,在它的类型没有被确定的情况下,它只能访问所有联合的类型的共用成员。
target.toObject(); // 报错
target.toString();
因此,我们使用 target 变量来调用 toObject 方法会报错,而调用 toString 方法不会报错。
这个时候有趣的事情就发生了,联合类型看似取的是所有类型的并集,但它只能访问所有类型成员的交集。
总结:交叉类型适合做对象的混入,联合类型可以是类型具有不确定性,增加代码的灵活度。
在 JS 中,我们通常会遇到这种场景,比如在一个对象中去获取某些属性的值,再来建立一个集合。我们来实现一下这个过程。
let obj = {
x: 1,
y: 2,
n: 3,
m: 4
};
function getValue(obj: any, keys: string[]) {
return keys.map(key => {
return obj[key];
});
}
console.log(getValue(obj, ['x', 'y'])); // [1, 2]
console.log(getValue(obj, ['a', 'b'])); // [undefined, undefined]
我们定义了一个对象,里面有一些属性。又定义了一个函数 getValue,用于获取对象 obj 中的某些属性值。我们去获取对象中存在的属性 x 和 y,就会得到正确结果。但如果访问对象中不存在的属性 a 和 b,就会输出 undefined,并不会报错。
如何使用 TS 对这种现象进行约束呢?就可以用到索引类型。要了解索引类型,我们先来了解下索引类型的相关概念。
"keyof T" 表示类型 T 所有公共属性的字面量联合类型。举个简单例子说明下:
interface Person {
name: string;
age: number;
}
let person: keyof Person; // 'name' | 'age'
我们定义了一个接口 Person,有两个成员 name 和 age,我们可以使用 keyof 来取出接口 Person 的成员的名字,keyof Person 即为字面量类型 'name' 和 'age' 的联合类型。
T[K] 的含义是接口 T 的成员 K 所代表的类型。我们再来看个例子:
interface Person {
name: string;
age: number;
}
let personProps: Person['age']; // number
这里我们指定 personProps 的类型为 Person.age 的类型,那么 personProps 的类型就是 number。
表示泛型变量可以通过继承某个类型获得某些属性。
接下来我们就来改造下 getValue 函数。
首先我们先把 getValue 改造成一个泛型函数,我们需要做一个些约束,这些约束就是 keys 里的元素必须是 obj 的属性。如何做这种约束呢?
我们先来将 getValue 写成泛型函数:
function getValue(obj: T, keys: K[]) {
return keys.map(key => {
return obj[key];
});
}
首先我们定义了一个泛型变量 T,来约束 obj。然后又定义了一个泛型变量 K,用来约束 keys 数组。
这里的 K 必须是一个字面量类型的联合类型 "'x' | 'y' | 'n' | 'm'",即为 keyof T。然后让 K 继承 T,即可达成我们想要的效果。
function getValue(obj: T, keys: K[]) {
return keys.map(key => {
return obj[key];
});
}
最后我们再来设置函数 getValue 的返回值类型为 T[K][]:
function getValue(obj: T, keys: K[]): T[K][] {
return keys.map(key => {
return obj[key];
});
}
T 表示我们传进来的 obj 的类型,T[k][] 则表示 getValue 的返回值是一个数组,数组中的成员类型必须要 obj 中属性的类型。
接下来来使用 getValue 函数:
console.log(getValue(obj, ['x', 'y'])); // [1, 2]
console.log(getValue(obj, ['a', 'b'])); // 报错
获取属性 x 和 y 的值可以正确输出。当我们试图去访问对象 obj 中不存在的属性 a 和 b 时,就会报错:Type '"a"' is not assignable to type '"x" | "y" | "n" | "m"'. Type '"b"' is not assignable to type '"x" | "y" | "n" | "m"'.,表示对象 obj 中不存在属性 a 和 b。
索引类型可以实现对对象属性的查询和返回,再配合泛型约束就能让我们使用对象或对象的属性以及属性值之间的一些约束的关系。