交叉类型
交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。
以 & 号进行分隔:Person & Serializable & Loggable,表示同时是 Person 和 Serializable 和 Loggable,就是说这个类型的对象同时拥有了这三种类型的成员。
由于其这个特性,我们不能将基本类型进行交叉。我们大多是在混入(mixins)或其它不适合典型面向对象模型的地方看到交叉类型的使用。
function extend<T, U>(first: T, second: U): T & U {
let result = <T & U>{};
for (let id in first) {
(<any>result)[id] = (<any>first)[id];
}
for (let id in second) {
if (!result.hasOwnProperty(id)) {
(<any>result)[id] = (<any>second)[id];
}
}
return result;
}
class Person {
constructor(public name: string) { }
}
interface Loggable {
log(): void;
}
class ConsoleLogger implements Loggable {
log() {
// ...
}
}
var jim = extend(new Person("Jim"), new ConsoleLogger());
var n = jim.name;
jim.log();
当我们使用基本类型进行交叉时:
let a: number & string;// 不会报错,但是你找不到任何值同时符合两个特性,无法赋值
a = 1 // 报错
a = '1' // 报错
a = undefined // 报错
联合类型
联合类型的意思是,这个变量可以是 A,也可以是 B,即可以是多个类型中的其中一个类型。比如传递的参数,我们允许传入数字或者字符串,此时就可以使用联合类型。
以 | 进行分隔:string | number。
let a:string|number = 'a';
a = 1;
字符串字面量类型
字符串字面量类型通过联合类型的形式限定了某个变量只能是其中一个字符串值。
let a = 'a' | 'b' | 'c';
数字字面量类型
数字字面量类型也是一样的
let a = 1 | 2 | 3;
可辨识联合类型
你可以合并单例类型,联合类型,类型保护和类型别名来创建一个叫做 可辨识联合的高级模式,它也称做 标签联合或 代数数据类型。 可辨识联合在函数式编程很有用处。 一些语言会自动地为你辨识联合;而TypeScript则基于已有的JavaScript模式。 它具有3个要素:
- 具有普通的单例类型属性 — 可辨识的特征。
- 一个类型别名包含了那些类型的联合— 联合。
- 此属性上的类型保护。
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}
首先我们声明了将要联合的接口。 每个接口都有 kind 属性但有不同的字符串字面量类型。 kind 属性称做可辨识的特征或标签。 其它的属性则特定于各个接口。 注意,目前各个接口间是没有联系的。 下面我们把它们联合到一起:
type Shape = Square | Rectangle | Circle;
现在我们使用可辨识联合:
function area(s: Shape) {
switch (s.kind) {
case "square": return s.size * s.size;
case "rectangle": return s.height * s.width;
case "circle": return Math.PI * s.radius ** 2;
}
}
完整性检查
当没有涵盖所有可辨识联合的变化时,我们想让编译器可以通知我们。 比如,如果我们添加了 Triangle 到 Shape,我们同时还需要更新 area:
type Shape = Square | Rectangle | Circle | Triangle;
function area(s: Shape) {
switch (s.kind) {
case "square": return s.size * s.size;
case "rectangle": return s.height * s.width;
case "circle": return Math.PI * s.radius ** 2;
}
}
上面代码中少了一个类型的判断,但是没有任何的提示。
我们可以使用两种方式来实现这种检查:
(1)首先是启用 --strictNullChecks
并且指定一个返回值类型
function area(s: Shape): number { // error: returns number | undefined
//...
}
由于没有涵盖所有情况,那么返回值类型会是 number 或者 undefined,而设置了返回值为 number,就会进行提示。
(2)使用 never 类型
function assertNever(x: never): never {
throw new Error("Unexpected object: " + x);
}
function area(s: Shape) {
switch (s.kind) {
//...
default: return assertNever(s); // 如果 s 不是 never 类型,这里就会进行提示
}
}
索引类型
在 JS 中,会有这样的场景:从一个对象中获取一些属性的值,然后进行结合。比如获取属性值形成一个数组。
let obj = {
a: 1,
b: 2,
c: 3
}
function getValue(obj: any, keys: string[]) {
return keys.map(key => obj[key]);
}
console.log(getValue(obj, ['a', 'b']))
当我们随意指定 obj 中没有的属性时,结果会得到 undefined 数组,并不会提示报错。
我们可以使用索引类型来实现。
- keyof T:索引类型的查询操作符,表示 T 类型的所有公共属性的字符串字面量类型
- T[K]:索引访问操作符,表示对象 T 的属性 K 的值所代表的类型
- T extends U:泛型约束,表示泛型变量 T 通过继承某个类型来获取一些属性约束
interface Obj {
a: numebr;
b: string;
}
let key: keyof Obj;
let value:Obj['a'];
改造前面的例子:
function getValue<T, K extends keyof T>(obj: T, keys: K[]): T[K][] { // 返回值是值的类型数组
return keys.map(key => obj[key]);
}
映射类型
一个常见的任务是将一个已知的类型每个属性都变为可选的或者只读的。TypeScript 提供了从旧类型中创建新类型的一种方式 — 映射类型,在映射类型里,新类型以相同的形式去转换旧类型里每个属性。
interface Obj {
a: string;
b: number;
c: boolean;
}
type ReadonlyObj = Readonly<Obj>; // 将 Obj 所有属性都转化为只读属性, 形成新的类型
type PartialObj = Partial<Obj>; // 将 Obj 所有属性都转化为可选属性, 形成新的类型
type PickObj = Pick<Obj, 'a' | 'b'>; // 将 Obj 中某些属性抽取出来, 形成新的类型
type RecordObj = Record<'x' | 'y', Obj>; // 新的类型将包含 x y 属性, 属性的类型都是后面指定的已知类型, 即 Obj
映射类型本质上是一些预先定义的泛型接口,可以供我们会直接使用:
type Readonly<T> = {
readonly [P in keyof T]: T[P];
}
type Partial<T> = {
[P in keyof T]?: T[P];
}
条件类型
条件类型类似于三元运算符:T extends U ? X : Y,即当 T 可以被赋值给 U 时,条件成立,返回 X 类型,否则返回 Y 类型。
type TypeName<T> = T extends string
? 'string'
: T extends number
? 'number'
: T extends boolean
? 'boolean'
: T extends undefined
? 'undefined'
: T extends Function
? 'function'
: 'object';
type T1 = TypeName<string>; //=> 'string'
type T2 = TypeName<string[]>; //=> 'object'
type T3 = TypeName<null>; //=> 'object'
如果 T 是联合类型,可以进行这样的拆解:
// (A | B) extends U ? X : Y 联合类型拆解
//=> (A extends U ? X : Y) | (B extends U ? X : Y)
type T4 = TypeName<string | string[]>; //=> 'string' | 'object'
我们可以使用条件类型实现类型过滤:
// 实现类型过滤
type Diff<T, U> = T extends U ? never : T;
type T5 = Diff<'a' | 'b' | 'c', 'a' | 'e'>;
// Diff<'a', 'a' | 'e'> | Diff<'b', 'a' | 'e'> | Diff<'c', 'a' | 'e'>
//=> never | 'b' | 'c'
//=> 'b' | 'c'
type NotNull<T> = Diff<T, undefined | null>; // 过滤 undefined 和 null
type T6 = NotNull<string | number | undefined>;
实际上 TS 已经内置了上面两种类型:
- Exclude
:过滤掉 T 中可以被赋值为 U 的类型 - NonNullable
:过滤掉 null 和 undefined - Extract
:只保留 T 中可以被赋值为 U 的类型,与 Exclude 相反