简介
TypeScript是js的超集,本篇主要梳理ts里面的原始类型、字面量类型、数组类型、函数类型、类类型、接口类型、类型别名、联合与交叉类型、枚举类型、泛型等类型元素,以及类型推断、类型断言、类型缩小、类型放大等特性。使用TypeScript,可以是我们编写的代码更加严谨,在编写代码的时候可以进行静态类型的校验。
安装TS
全局安装typescript:
npm i -g typescript
创建一个空的文件夹,用来存放ts代码,然后进入项目文件夹,初始化ts项目:
tsc -init
此时项目文件夹下会生成一个tsconfig.json配置文件,ts编译成js会根据这个文件的配置进行处理。
全局安装ts-node,用来直接运行ts文件:
npm i -g ts-node
原始类型
string,number,boolean,symbol,null,undefined
let str: string = "2"; // string
let num: number = 1; // number
let bool: boolean = true; // boolean
let sy: symbol = Symbol(); // symbol
let nul:null = null; // null
let undef: undefined = undefined; // undefined
另外:
let vd: void = undefined; // 可以把undefined类型赋值给void类型,但是反过来不行
// 函数没有返回值,那么函数的返回值类型就是void
function fn(): void {
return undefined;
}
注意:
- void只用在函数没有返回值的情况。
- 如果在定义变量时候不指定类型,typescript可以进行推断,按首次初始化的值类型作为变量类型。
非原始类型
小object、大Object、{}
小object:代表的是非原始类型的类型,也就是不能将string,number,boolean,symbol等原始类型的值赋值给object类型的变量。严格模式下,null和undefined也不能赋值给object类型的变量。
let obj1: object = 3; // 报错
let obj2: object = "3"; // 报错
let obj3: object = true; // 报错
let obj4: object = null; // 报错
let obj5: object = undefined; // 报错
let obj6: object = Symbol(); // 报错
let obj7:object = {a: 1, b: '2'};
let obj8:object = [1, 2, 3];
大Object :代表所有拥有 toString、hasOwnProperty 方法的类型,所以所有原始类型、非原始类型都可以赋值给Object类型的变量。严格模式下,null和undefined不可以赋值给Object类型的变量。
let obj1: Object = 3;
let obj2: Object = "3";
let obj6: Object = Symbol();
let obj3: Object = true;
let obj4: Object = null; // 报错
let obj5: Object = undefined; // 报错
let obj7: Object = { a: 1, b: "2" };
let obj8: Object = [1, 2, 3];
{}:{}空对象类型和大 Object 一样。
数组类型
数组类型的定义:
// 方法一:指定数组内的元素都是数字
let arr1: Array<number> = [1, 2, 3];
arr1.push('3'); // 报错
arr1.push(5);
// 方法二:指定数组内的元素都是字符串
let arr2: string[] = ['4', '5', 'a'];
arr2[3] = '6';
arr2[3] = 1; // 报错
// 方法三:通过元组的方式定义,可以指定数组内元素的不同类型
// 此方法只能是长度、类型明确的数组
let arr3: [number, number, boolean] = [10, 20, true]
// 方法四:定义任意长度、任意类型的数组
let arr4: any[] = [1, 2, "3"];
字面量类型
字面量不仅可以表示值,还可以表示类型
// 指定变量x1的值是1或3,其他赋值报错
let x1: 1 | 3 = 1;
x1 = 2; // 报错
x1 = 3;
// 指定变量x2的值是hello,其他赋值报错
let x2: 'hello' = 'hello';
x2 = 'hi'; // 报错
// 定义Hello类型,通过Hello约束的变量值只能是hello
type Hello = 'hello';
let h1: Hello = 'hello';
h1 = 'hi'; // 报错
注意:TypeScript 支持 3 种字面量类型:string字面量类型、number字面量类型、boolean字面量类型
联合类型
可以把“|”类比为 JavaScript 中的逻辑或 “||”,只不过前者表示的是可能的类型。
// 或标识符|
// 表示要么有a属性,要么有b属性,不能有其他属性
// 要么是数字类型,要么是字符串类型
let a: number | string = 20;
a = true; // 报错
let obj: { a: 1 } | { b: '3' };
obj = { a: 1 };
obj = { a: 1, b: '3' };
obj = { b: '3' };
obj = { a: 4 }; // 报错
obj = { c: '3' }; // 报错
交叉类型
交叉类型(Intersection Type)类似逻辑与的行为,可以把多个类型合并成一个类型,合并后的类型将拥有所有成员类型的特性。
使用&
操作符来声明交叉类型。
// 这样定义是没意义的,因为没有一个值满足即是字符串类型也是数字类型!!
let m : string & number;
// zs变量的值需包含所有成员类型的属性
let zs: { name: string; age: number } & { height: number } = {
name: "张三",
age: 20,
height: 180,
};
联合类型、交叉类型组合
联合、交叉类型本身可以直接组合使用,这就涉及 |
、&
操作符的优先级问题。
联合操作符 |
的优先级低于交叉操作符 &
。同样,我们可以通过使用小括弧 () 来调整操作符的优先级,这个和js一样。
let m1: { id: number } & { name: string } | { id: string } & { name: number };
m1 = { id: 1, name: '' }
m1 = { id: '', name: 1 }
let m2: ({ id: number } | { id: string }) & ({ name: string } | { name: number });
m2 = { id: 1, name: '' }
m2 = { id: '', name: 1 }
any和unknown
- any 指的是一个任意类型,会跳过类型检查器对值的检查,所以任何值都可以赋值给any类型。它是官方提供的一个选择性绕过静态类型检测的作弊方式。非常不建议使用;Any is Hell(Any 是地狱) 。
- unknown 是 TypeScript 3.0 中添加的一个类型,它主要用来描述类型并不确定的变量。就是会进行类型检测。
any和unknown的区别:
- 任何类型的值可以赋值给any,同时any类型的值也可以赋值给任何类型。
- 任何类型的值都可以赋值给unknown,但unknown类型的值只能赋值给unknown类型和any类型。
let unk: unknown;
let x = 1;
let y = "2";
if (x) {
unk = x;
} else {
unk = y;
}
// 使用unknown后,typescript会做类型检测
unk.toFixed(2); // 报错
// 通过缩小类型可以通过类型检测
if (typeof unk === 'number') {
unk.toFixed(2);
}
// any会绕过类型检测,所以下面不会有问题提示
let an1: any;
an1.toFixed(2);
注意:unknown比any好的地方,还有一个就是它可以通过缩小类型的手段类确定类型
never类型
never 类型表示的是那些永不存在的值的类型。
值会永不存在的两种情况:
- 如果一个函数执行时抛出了异常,那么这个函数永远不存在返回值(因为抛出异常会直接中断程序运行,这使得程序运行不到返回值那一步,即具有不可达的终点,也就永不存在返回了)
- 函数中执行无限循环的代码(死循环),使得程序永远无法运行到函数返回值那一步,永不存在返回。
// 异常
function error(msg: string): never {
throw new Error(msg);
}
// 死循环
function loopForever(): never {
while (true) { };
}
注意:never 是所有类型的子类型
interface接口类型
TypeScript 不仅能帮助前端改变思维方式,还能强化面向接口编程的思维和能力,而这正是得益于 Interface 接口类型。
定义变量和函数的类型
// 使用interface关键字定义接口
interface PersonInfo {
name: string;
age: number;
}
// 定义变量的类型
let zhangsan: PersonInfo = {
name: "张三",
age: 20,
};
// 定义数组的类型
interface ArrayNumber {
[idx: number]: number;
}
let arr1: ArrayNumber = [1, 2, 3];
// 定义函数的类型
interface PersonFn {
(p: PersonInfo): void;
}
let Person1: PersonFn = (obj: PersonInfo): void => {
console.log(obj.name, obj.age);
};
继承
多个不同接口之间是可以实现继承的,组合成一个新的接口。但是如果继承的接口PersonInfo和被继承的接口NameInfo有相同的属性,并且类型不兼容,那么就会报错。
interface NameInfo {
name: string;
}
interface AgeInfo {
age: number;
}
interface PersonInfo extends NameInfo, AgeInfo {
// name: number; // 会报错,因为和NameInfo中定义的name属性冲突
height: number;
}
let zs: PersonInfo = {
name: "张三",
age: 20,
height: 177,
};
多个相同的接口
多个相同名字的接口,会进行合并,得到一个新的接口;这个接口的特性一般用在扩展第三方库的接口类型。
interface PersonInfo {
name: string;
age: number;
}
interface PersonInfo {
name: string;
height: number;
}
let zs: PersonInfo = {
name: "张三",
age: 20,
height: 177,
};
缺省和只读属性
interface PersonInfo {
name?: string; // 指定该属性是可选的
readonly height: number; // 指定该属性是只读的
}
类型别名type
接口类型的一个作用是将内联类型抽离出来,从而实现类型可复用。同时,也可以使用类型别名接收抽离出来的内联类型实现复用。
格式:type
别名名称 = 类型定义。
基础使用
type PersonInfo = { name: string; age: number; };
let zs: PersonInfo = {
name: "张三",
age: 20,
};
特定使用场景
到这里可能觉得类型别名和接口没多大区别,这不是重复了吗?其实不是,类型别名可以针对接口没法覆盖的场景,例如组合类型、交叉类型等;
// 1. 组合类型
type NumAndString = number | string;
// 2. 交叉类型
type SectionType = { name: string; age: number } & {
height: number;
name: string;
};
interface PersonInfo {
name: string;
height: number;
}
// 3. 提取接口属性类型
type PersonHeight = PersonInfo["height"];
let zs: SectionType = {
name: "张三",
age: 20,
height: 180,
};
Interface 与 Type 的区别
type可以声明基本数据类型别名/联合类型/元组等,但interface不可以。
// 基本类型别名
type UserName = string;
type UserName = string | number;
// 联合类型
type Animal = Pig | Dog | Cat;
type List = [string, boolean, number];
interface能够合并声明,但type不可以。基于此特性可以很方便地对全局变量、第三方库的类型做扩展。
// 此时Person同时具有name和age属性
interface Person {
name: string
}
interface Person {
age: number
}
// 定义类型别名,会报错。
type NewType = number | string; // 报错
type NewType = boolean; // 报错
注意:
- interface更多的是为我们的应用创建数据模型,以进行类型检测。
- type更多的是为我们需要使用的单个或一组类型创建一个引用,起一个别名,通过这个别名使用单个或一组类型。
函数类型
基础定义
显式指定函数参数和返回值的类型:
const add = (a: number, b: number): number => {
return a + b;
}
或者用type来声明函数类型:
type addFnType = (a: number, b: number) => number;
let addFn: addFnType = (num1, num2) => {
return num1 + num2;
}
或者用接口来声明函数类型:
// 接口定义函数类型
interface FnItf {
// 形参类型:返回值类型
(p: string): number
}
let fn: FnItf = (p: string) => {
return 1
}
fn("")
当函数作为对象的属性出现的时候:
// 函数作为对象的属性出现的时候
// 接口
interface FnItf {
(p: string): number
}
interface ObjItf {
fn: FnItf
}
let obj1: ObjItf = {
fn: (str) => {
console.log(str)
return 1
}
}
obj1.fn("")
// 类型别名
type ObjType = { fn: (p: string) => number }
let obj2: ObjType = {
fn: (str) => {
console.log(str)
return 1
}
}
obj2.fn("")
函数参数类型
参数一般有:可选参数、默认参数、剩余参数;
- 可选参数
在类型标注的:
前添加?
表示函数的参数就是可缺省的;
function log(msg?: string):void {
console.log(msg);
}
注意:可缺省并不是相当于msg参数的类型就是和string | undefined
等价的。string | undefined
的意思是这两个类型中的一种,而可缺省是不传的意思。
- 默认参数
function addFn1(num1: number = 1, num2: number = 2):number {
return num1 + num2;
}
函数的默认参数类型必须是参数类型的子类型:
function log2(x: number | string = 'hello') {
console.log(x);
}
// 这里x参数的类型就是联合类型`number | string`,函数默认参数的类型就是联合类型的子类型
- 剩余参数
function sum(...nums: number[]) {
return nums.reduce((a, b) => a + b, 0);
}
sum(1, 2); // => 3
sum(1, 2, 3); // => 6
- this
函数中的this问题,一直都是javascript最令人头疼的问题,因为this的指向只有函数调用的时候才能确定。还有一些可以改变this指向的方法(apply,call,bind)。
但是在Typescript中,必须要明确的指定this的类型(严格模式下)。
type objType = {
person: (n: string) => void,
myname: string
};
function person(this: Window | objType, name: string): void {
this.myname = name;
console.log(this.myname);
}
window.person = person;
window.person('window name');
let obj: objType = {
person,
myname: ''
};
obj.person('obj name');
单单是上面的代码是有问题的,我们还需要创建一个类型声明文件global.d.ts,为window对象上扩展两个属性person、myname;
interface Window {
person: (n: string) => void;
myname: string;
}
定义对象的函数属性时,只要实际调用中 this 的指向与指定的 this 指向不同,TypeScript 就能发现 this 指向的错误
interface ObjType2 {
name: string;
say: (this: ObjType2) => void;
}
let obj2: ObjType2 = {
name: 'obj2',
say() {
console.log(this.name);
}
}
obj2.say(); // ok
let t11 = obj2.say;
t11(); // 报错
注意:
- 显式声明函数的返回值类型为 undfined,则会出现错误提示,如果没有返回值,我们用void表示;
- 显式注解函数中的 this 类型,它表面上占据了第一个形参的位置,但并不意味着函数真的多了一个参数,因为 TypeScript 转译为 JavaScript 后,“伪形参” this 会被抹掉,这算是 TypeScript 为数不多的特有语法。
枚举
数字枚举
枚举的作用在于定义被命名的常量集合。
一个默认从 0 开始递增的数字集合,称之为数字枚举。
enum Days {
Sunday = 1, // 指定从初始值
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday
}
let day1 = Days.Sunday; // 1
let day2 = Days.Monday; // 2
字符串枚举
字符串枚举必须为每一项赋初始值。
enum Days {
Sunday = 'Sunday',
Monday = 'Monday',
Tuesday = 'Tuesday',
Wednesday = 'Wednesday',
Thursday = 'Thursday',
Friday = 'Friday',
Saturday = 'Saturday',
}
let day = Days.Monday; // Monday
泛型
泛型指的是类型参数化,即将原来某种具体的类型进行参数化。设计泛型的目的在于有效约束类型成员之间的关系,比如函数参数和返回值、类或者接口成员和方法之间的关系。
泛型类型参数
function getValue(val: string): string {
return val;
}
function getValue1(val: number): number {
return val;
}
function getValue2(val: unknown): unknown {
return val;
}
let g1: string = getValue("1");
let g2: number = getValue1(1);
let g3: unknown = getValue2(1);
// 通过泛型优化
function getValue3<T>(val: T): T {
return val;
}
let g4: number = getValue3<number>(3);
let g5: string = getValue3<string>('4');
泛型类型
前面使用过Array<类型>
来定义数组的类型,这里的Array
也是一种类型,<类型>
相当于给Array
类型传递类型
参数,指定内部元素的类型。
在 TypeScript 中,类型本身就可以被定义为拥有不明确的类型参数的泛型,并且可以接收明确类型作为入参,从而衍生出更具体的类型。
// 定义数组类型
let arr: Array<number> = [1];
let arr1: Array<string> = [""];
// 类型别名
type typeFn<P> = (params: P) => P;
let fn1: typeFn<number> = (n: number) => {
return n;
};
let fn2: typeFn<string> = (p: string): string => {
return p;
}
// 定义接口类型
interface TypeItf<P> {
name: P;
getName: (p: P) => P;
}
let t1: TypeItf<number> = {
name: 123,
getName: (n: number) => {
return n;
},
};
let t2: TypeItf<string> = {
name: "123",
getName: (n: string) => {
return n;
},
};
泛型约束
把泛型入参限定在一个相对更明确的集合内,以便对入参进行约束。
interface TypeItf<P extends string | number> {
name: P;
getName: (p: P) => P;
}
let t1: TypeItf<number> = {
name: 123,
getName: (n: number) => {
return n;
},
};
let t2: TypeItf<string> = {
name: "123",
getName: (n: string) => {
return n;
},
};
class(类)
面向对象 OOP 编程思想,在实际工作中,都是极其有用的抽象、封装利器。
class Person {
name: string;
say(this: Person, song: string): Person {
console.log(song);
return this;
}
constructor(name: string) {
this.name = name;
}
}
let p1 = new Person('张三');
p1.say('Song').name;
继承
使用extends关键字实现继承
class Male extends Person {
age: number;
constructor(name: string, age: number) {
super(name);
this.age = age;
}
}
修饰符(public、private、protected、readonly)
在 TypeScript 中就支持 3 种访问修饰符,分别是 public、private、protected。通过这三个修饰符做到控制属性和方法的访问。
- public:基类、子类、类外部都可以访问;
- protected:基类、子类可以访问,类外部不可以访问;
- private:基类可以访问,子类、类外部不可以访问;
- readonly:只读修饰符
class Person {
public readonly name: string = '张三';
protected age: number = 20;
private height: string = '180';
protected getPersonInfo(): void {
console.log(this.name, this.age, this.height); // 基类里面三个修饰符都可以访问
}
}
class Male extends Person {
public getInfo(): void {
console.log(this.name, this.age); // 子类只能访问public、protected修饰符的
}
}
let m = new Male();
console.log(m.name); // 类外部只能访问public修饰的
m.name = '李四'; // name属性使用只读修饰符,所以不能对name进行赋值修改操作
静态属性
基于静态属性的特性,往往会把与类相关的常量、不依赖实例 this 上下文的属性和方法定义为静态属性,从而避免数据冗余。
class Person {
static title: string = "个人信息";
}
Person.title;
抽象类和接口
抽象类,它是一种不能被实例化仅能被子类继承的特殊类。
abstract class Person {
abstract name: string;
abstract getName(): void;
extendsFn(): void {
console.log('扩展方法');
}
}
class Male extends Person {
constructor(name: string) {
super();
this.name = name;
}
name: string;
getName(): void {
console.log(this.name);
}
}
接口interface也可以约束类的实现,使用接口与使用抽象类相比,区别在于接口只能定义类成员的类型。
interface Person {
name: string;
age: number;
getName: () => void;
}
class Male implements Person {
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
name: string;
age: number;
getName(): void {
console.log(this.name);
}
}
类的类型
在声明类的时候,其实也同时声明了一个特殊的类型,这个类型的名字就是类名,表示类实例的类型。
class Male {
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
name: string;
age: number;
getName(this: Male): void {
console.log(this.name);
}
}
let m1: Male = new Male("张三", 20);
let m2: Male = {
name: "张三",
age: 20,
getName(this: Male) {
console.log(this.name);
},
};
m2.getName();
let fn = m2.getName;
fn(); // 报错,this指向并不是Male对象