前言
TypeScript真香系列的内容将参考中文文档,但是文中的例子基本不会和文档中的例子重复,对于一些地方也会深入研究。另外,文中一些例子的结果都是在代码没有错误后编译为JavaScript得到的。如果想实际看看TypeScript编译为JavaScript的代码,可以访问TypeScript的在线编译地址,动手操作,印象更加深刻。
交叉类型
交叉类型是将多个类型合并为一个类型,相当于一种并的操作。
interface IDog {
name: string,
age: number,
}
interface ICat {
name: string,
color: string
}
let animal: IDog & ICat;
animal = {
name: "哈士奇",
age: 1,
color: "white",
}
animal.name; // "哈士奇"
animal.age; // 1
上面animal中的属性一个都不能少,如果少了属性的话,就会出现下面的错误:
i
nterface IDog {
name: string,
age: number,
}
interface ICat {
name: string,
color: string
}
let animal: IDog & ICat;
animal = { //错误,color属性在ICat中是必须的
name: "哈士奇",
age: 1,
// color: "white",
}
联合类型
联合类型可以说是和交叉类型相反,声明的类型不确定,可以是多个类型中的一个或几个。
let a: number | string;
a = 1;
a = "s";
a = false; // 错误,类型false不能分配给类型 number | string
看一个和交叉类型相对应的例子:
interface IDog {
name: string,
age: number,
}
interface ICat {
name: string,
color: string
}
let animal: IDog | ICat; // 这里我们把&改成了|
animal = { //没有报错
name: "哈士奇",
age: 1,
// color: "white",
}
animal.name;
animal.age;
再看一个例子:
interface IDog {
name: string,
age: number,
}
interface ICat {
name: string,
color: string
}
let animal: IDog | ICat;
animal = {
name: "哈士奇",
age: 1,
color: "white",
}
animal.name;
animal.age; //错误,age不存在于ICat. age不存在于IDog | ICat
我们可以看见上面的例子出现了错误,这是因为TypeScript编译器age不知道是IDog还是ICat,所以只能访问公共的name属性。如果我们想要访问这个属性的话,该怎么办?我们可以使用类型断言:
interface IDog {
name: string,
age: number,
}
interface ICat {
name: string,
color: string
}
let animal: IDog | ICat;
animal = {
name: "哈士奇",
age: 1,
color: "white",
}
animal.name;
(<IDog>animal).age; // 1
这下就能访问age属性了。
类型保护
有时候我们会遇到类似于下面这种场景:
interface IDog {
name: string,
age: number,
}
interface ICat {
name: string,
color: string
}
function animal(arg: IDog | ICat): any {
if (arg.color) { //错误
return arg.color //错误
}
}
但是上面的代码会出现错误。如果想要上面这段代码正常工作,可以和联合类型中的例子一样,使用类型断言:
interface IDog {
name: string,
age: number,
}
interface ICat {
name: string,
color: string
}
function animal(arg: IDog | ICat):any {
if ((<ICat>arg).color) {
return (<ICat>arg).color;
}
}
除了类型断言,我们还可以利用类型保护来进行判断,常用的类型保护有三种:typeof类型保护,instanceof类型保护和自定义类型保护。
typeof类型保护
function animal(arg: number | string): any {
if (typeof arg === "string") {
return arg + "猫";
}
}
typeof类型保护只有两种形式能被识别: typeof v === "typename"
和 typeof v !== "typename"
。"typename"必须是 "number", "string", "boolean"或 "symbol"。
instanceof类型保护
class Dog {
name: string;
age: number;
constructor() {
};
}
class Cat {
name: string;
color: string;
constructor() {
};
}
let animal: Dog | Cat = new Dog();
if (animal instanceof Dog) {
animal.name = "dog";
animal.age = 6;
}
if (animal instanceof Cat) {
animal.name = "cat";
animal.color = "white";
}
console.log(animal); //Dog {name: "dog", age: 6}
instanceof的右侧要求是一个构造函数,TypeScript将细化为:
- 此构造函数的 prototype属性的类型,如果它的类型不为 any的话;
- 构造签名所返回的类型的联合。
自定义类型保护
对于一些复杂的情况,我们可以自定义来进行类型保护:
interface IDog {
name: string,
age: number,
}
interface ICat {
name: string,
color: string
}
let animal: IDog | ICat;
animal = {
name: "哈士奇",
age: 6,
}
function isDog(arg: IDog | ICat): arg is IDog {
return arg !== undefined;
}
if (isDog(animal)) {
console.log(animal.age); //6
}
类型别名
类型别名可以给类型取一个别名。类型别名和接口类似,但又有不同。
type Name = number;
type Types = number | string;
type NAndT = Name & Types;
type MyFunc = () => number;
function animal(arg: Types) {
return arg;
}
animal("哈士奇"); //"哈士奇"
类型别名可以作用于原始类型、联合类型、泛型等等。
type Dog<T> = { value: T };
function dog(arg: Dog<string>) {
return arg;
}
dog({ value: "哈士奇" }); //{value: "哈士奇"}
dog({ value: 1}); //错误,类型number不能分配给string
dog("哈士奇"); //错误,参数“哈士奇”不能分配给类型 Dog<string>
接口和类型别名的区别
区别一:接口可以创建新的名字,而且可以在其它任何地方使用;类型别名不创建新的名字,而是起一个别名。
区别二:类型别名可以进行联合,交叉等操作。
区别三:接口可以被extends和implements以及声明合并等,而类型别名不可以。
这里介绍一下声明合并:
“声明合并”是指编译器将针对同一个名字的两个独立声明合并为单一声明。 合并后的声明同时拥有原先两个声明的特性。
任何数量的声明都可被合并;不局限于两个声明。
举个例子:
interface IDog {
name: string;
setName(arg:string): string;
}
interface IDog {
age: number;
}
interface IDog {
color: string;
}
let dog: IDog;
dog = {
color: "black",
age: 6,
name: "哈士奇",
setName: (arg) => {
return arg
}
}
合并之后:
interface IDog {
color: string;
age: number;
name: string;
setName(arg:string): string;
}
我们可以看出,后面的接口在合并后出现在了靠前的位置。
字符串字面量类型和数字字面量类型
字符串字面量允许我们指定字符串为必须的固定值。
type Dog = "哈士奇" | "泰迪" | "中华田园犬" | "萨摩耶";
function dog(arg: Dog):any {
switch (arg) {
case "哈士奇":
return "傻狗";
case "泰迪":
return "精力旺盛";
case "中华田园犬":
return "忠诚";
case "萨摩耶":
return "微笑天使";
}
}
dog("哈士奇"); //"傻狗"
dog("柯基"); //错误,参数"柯基"不能分配给类型Dog
数字字面量同理。
type Num = 1 | 2 | 3 | 4 | 5 | 6 | 7;
function week(arg: Num):any {
switch (arg) {
case 1:
return "星期一";
case 2:
return "星期二";
case 3:
return "星期三";
case 4:
return "星期四";
case 5:
return "星期五";
case 6:
return "星期六";
case 7:
return "星期日";
}
}
week(6); //"星期六"
week(8); //错误
可辨识联合
我们可以合并单例类型、联合类型、类型保护和类型别名来创建一个叫做可辨识联合的高级模式。它具有三个要素:
- 有普通的单例类型属性— 可辨识的特征。
- 一个类型别名包含了那些类型的联合— 联合。
- 此属性上的类型保护。
可以看看下面这个例子就能理解了:
//我们首先声明了将要联合的接口,目前各个接口之间是没有联系的,
//只是都有一个kind的属性(可以称为可辨识特征或标签)但是有不同的字符串字面量类型
interface IColor {
kind: "color";
value: string;
}
interface ISize {
kind: "size";
height: number;
width: number;
}
//然后我们利用类型别名和联合类型把两个接口联合到一起
type MyType = IColor | ISize;
//最后使用可辨识联合
function types(arg: MyType) :any{
switch (arg.kind) {
case "color":
return arg.value;
case "size":
return arg.height * arg.width;
}
}
types({ kind: "color", value: "blue" }); //"blue"
types({ kind: "size", height: 10, width: 20 }); //200
索引类型
使用索引类型,编译器就能够检查使用动态属性名的代码。我们首先要知道两个操作符。
- 索引类型的查询操作符
keyof T
,意为:对于任何类型T
,keyof T
的结果为T
上已知公共属性的联合; - 索引访问操作符
T[K]
。
interface IDog{
a: string,
b: number,
c: boolean,
}
let dog: keyof IDog; //let dog: "a" | "b" | "c"
let arg: IDog["a"]; //let arg: string
再看一个较为复杂的例子:
interface IDog{
name: string,
age: number,
value: string,
}
let dog: IDog;
dog = {
name: "二哈",
age: 6,
value: "奥里给"
}
function myDog<T, K extends keyof T>(x: T, args: K[]): T[K][] {
return args.map(i => x[i]);
}
myDog(dog, ['name']); //["二哈"]
myDog(dog, ['name', 'value']); //["二哈", "奥里给"]
myDog(dog, ['key']); //错误,类型不匹配
映射类型
有时候我们可以会遇到这种情况,把每个成员都变为可选或者只读:
interface Person{
name: string;
agent: number;
}
interface PersonPartial {
name?: string;
age?: number;
}
interface PersonReadonly {
readonly name: string;
readonly age: number;
}
这在JavaScript里经常出现,TypeScript提供了从旧类型中创建新类型的一种方式 — 映射类型。 在映射类型里,新类型以相同的形式去转换旧类型里每个属性。
interface IPerson{
name: string;
agent: number;
}
type OPartial<T> = {
[P in keyof T]?: T[P];
}
type OReadonly<T> = {
readonly [P in keyof T]: T[P];
}
//使用方式
type PersonPartial = OPartial<IPerson>;
type ReadonlyPerson = OReadonly<IPerson>;
参考
https://github.com/zhongsp/Ty...
https://github.com/jkchao/typ...
最后
文中有些地方可能会加入一些自己的理解,若有不准确或错误的地方,欢迎指出~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。