TypeScript,简称TS,就是给JS添加了类型系统,你可能会问,为什么要给JS添加类型系统呢?因为JS用法太过灵活了,3+[] 经过一系列的类型转化,竟然得到了字符串“3”, 如果程序出现问题,这种错误很难debug。加了类型,就能解决这类问题吗?是的,因为一旦定义了类型,你就知道,这个类型可以进行哪个操作,比如,number类型,可以进行算术运算,如果进行非法操作,就会报错。类型,就是定义取值的范围,以及对这些值的操作。学习TypeScript就是学习它的类型系统。学习之前,一定要先安装它, mkdir ts-learning && cd ts-learning && npm int -y 新建ts-learning项目, npm install typescript -D安装ts. touch type.ts,就可以写TypeScript代码了。
TypeScript定义了各式各样的类型,如number, string, boolean等。那怎么给变量指定类型呢?格式为 变量名:变量类型。
let str: string; let a: number = 3;
如果在声明变量时,进行了初始化操作,像let a:number = 3; a是可以不用指定类型的,TypeScript 能够自动推断出a变量的类型是number类型。let a = 3; 当你把鼠标放到变量a上时,TypeScript 推断它是number类型。
这也算是一种最佳实践吧,尽量让TypeScript 去推断类型,只有当TypeScript 不能正确地推断时,我们才指定类型。
使用了类型系统后,程序要怎么运行呢?TS代码是不能直接运行的,要把它编译成JS代码,然后在浏览器或node 中执行。TypeScript 提供了tsc编译器,编译TS代码 到JS,当你npm install typescript 时,同时安装了tsc 编译器。但你要告诉它怎么编译,比如源文件在什么地方,编译到什么地方等等。这些信息放置的地方就是TS的配置文件tsconfig.json, 要把它放到项目的根目录下。执行tsc命令时,它会从项目根目录中寻找tsconfig.json, 然后根据配置信息进行编译。最简单的配置文件如下:
{ "compilerOptions": { "rootDir": "./src", /* .ts文件的根目录 */ "outDir": "dist", /* 编译后的文件放到哪里 */ "target": "ES2015", /* 编译到目标语法*/ "module": "commonjs" /* 编译后文件使用哪一种规范*/ } }
npx tsc 进行编译,项目就多出来了dist目录。node dist/type.js就可以运行程序了。假设粗心大意,给str赋值了数字3,
let str: string; str = 3;
你会发现str变量下面标红了,鼠标移动上去,看到了报错的原因,数字类型不能赋值给字符串类型。怎么这么神奇呢?npm install typescript,除了安装了刚才说的tsc编译器,还安装了tsserver(TypeScript Language Service), 编辑器或IDE利用tsserver来完成标红提示错误等功能。当你编写TS代码时,编辑器会时时地与tsserver 进行通信,实实编译代码(编译到内存中)。这里要注意的是VS Code使用的TS版本,它默认使用的是Node自带的tsc编译器,当你打开任意一个.ts文件时,在VS Code的右下方,你都能看到这个版本号,
如果想使用自己安装的TS版本,就点击这个版本号,VS Code会在编辑区的上方弹出一个选择框,点击选择想要的版本就可以了。说的有点远了,现在再执行npx tsc会发生什么?同样报错,tsc 在编译的过程中,它会类型检查(type check),但你也发现, tsc 仍然把错误的TS代码编译成了JS代码。按理说,如果有错,就不应该编译,更不应该输出编译后的JS文件。 幸好,可以在tsconfig.json配置noEmitOnError: true, 如查TS检查出错误,它就不会编译JS代码。
无论是VS Code编辑器的实时编译,还是tsc 的编译,你都会发现TS给JS加入类型系统的一个好处,就是尽可能早的发现问题,就是尽量把运行时发生的错误在编译时捕获,不用运行代码,也能检测到在程序运行过程中引起问题的代码。
number, string, boolean都是JS中拥有的类型,行为也类似,就不说了,主要说TypeScript中定义的JS中没有的类型。
any: 任意类型
当一个变量是any类型时,它可以被赋值给任何值。
let str: any; str = 2; str = 'str';
同时,any类型的值也可以赋值给任意其它类型。
let a: any = 'string'; let b: number = a;
当使用any类型时,TS不会进行类型检查,代码很容易出错,所以尽量不要用any 类型。
unknown:不知道的类型
为什么会有这种类型呢?在极少的情况下,你事先并不知道一个变量要保存什么类型,需要一个类型标注。你可能会说不是有any吗?确实,可以声明为any类型,但一旦声明为any类型,这个变量就不受控制了,就可以对这个变量进行任何操作,但你肯定不想对一个变量进行任意的操作,报错了怎么办?unknown类型和any类型不同之处就是在于控制操作上。当一个变量是unkonwn类型时,你做的事情并不多,如果要使用unknown类型,在使用之前,一定缩小它的类型范围(narrow type)。举个例子,就明白了
let a: any; a = 3; console.log(a.toUpperCase()); let b: unknown; b = 3;
// Property 'toUpperCase' does not exist on type 'unknown'. console.log(b.toUpperCase());
假设事先并不知道a和b的类型,于是分别把a和b标注了any 和unkown类型。先看a(any类型),你可以把任何值赋给它,于是赋值了3,你可以对其进行任何操作,于是调用方法toUpperCase(),实际上数字3并没有toUpperCase()方法,尝试去调用对象不存在的方法,就会导致程序运行时报错。
再看b,编译器直接报错了,toUpperCase属性不存在unknown类型上。这就是区别。当你声明一个unknown类型的变量时,在对其进行操作(调用方法)之前,编译器会强制你进行向下类型缩小,比如你要对这个类型进行判断,有没有这个属性,这就减小了潜在的运行时报错。缩小类型范围的方法有很多,这里使用typeof, 我们都知道只有字符串才调用toUpperCase()方法,那就判断b是不是字符串
let b: unknown; b = 3; if (typeof b === 'string'){ console.log(b.toUpperCase()); }
unknown和any一样,可以表示任意的值 ,但后续的操作却完全不同。any可以进行任意的操作,但unknown却要进行类型缩小,也因此更为安全。如果实在是不知道变量的类型,尽量使用unknown,不要使用any。
null和undefined 类型
它们也是JS中已有的类型,之所以把它们拿出来说,是因为,在TypeScript中,null和undefined可以赋值给其它任意类型,而不仅仅是null和undefined类型。
const x: number = null;
竟然没有报错,x是number类型,值却是null,如果用x调用number的方法,肯定报错,最好是null只能赋值给null类型,undefined只能赋给undefined类型。这要配置strictNullChecks来控制null或undefined是否能赋值给其它类型。strictNullChecks: true 表示禁止赋值给其它类型。当在tsconfig.json中写入strictNullChecks: true
// Type 'null' is not assignable to type 'number' const x: number = null;
如果变量值就是null,那要明显地标注变量是null类型
const x: null = null;
字面量类型
TypeScript 还定义了字面量类型,3也是一个类型。变量类型的声明方式是 变量名:类型,如果类型是一个字面量时,就是字面量类型。
let a: 3; let b: 'name'; let c: false;
当变量类型是字面量类型时,它的取值也就固定了。a只能赋值为3,b只能赋值为'name',c只能赋值为false。
数组类型
在使用数组时,数组中的元素通常是一个类型,所以数组的类型声明是元素的类型后面跟上[]
let arr: number[]; // arr数组中每一个元素都是number类型
数组还有一个子类叫元组类型,看一下类型声明就清楚了
let turple: [number, string];
[number, string] 就是一个元组类型,[ ]表示它是一个数组类型,但元素的类型声明不是在[ ] 的外面,而是在里面。这表明,数组中元素都是固定的,也就是数组的长度是固定的,并且数组中的每一个元素的类型也是固定的,所以元组就是一个元素类型固定且长度固定的数组。turple 就是2个元素的数组,数组的第一个元素的类型是number,第二个元素的类型是string。
let arr: number[] = [2, 3, 4, 5]; let turple: [number, string] = [2, 'name'];
对象类型
在TS中有4种方式来标示一个变量(或值)是对象类型
let obj1: object = {a: 2}; // 小写object 类型 let a: {b: number} = { b: 12 } //对象字面量语法,{b: number}类型 let danger: {} // {} 类型 let obj: Object; // 大写的object类型。
先看第一种,小写的object类型。
let obj1: object = {a: 2}; // Property 'a' does not exist on type 'object' obj1.a;
获取obj1对象上a属性,发现报错了,object类型上不存在属性a。当声明一个变量是object类型时,只表示它是一个object,而不是null,再也没有其它过多信息了。你对object类型(obj1)什么也做不了。
再看{} 类型和Object类型。
let danger: {}; danger = {}; danger = {x: 1} danger = []; danger = 2; let Obj: Object; Obj =1; Obj = {x: 1};
除了null和undefined以外的类型都能赋值{} 类型和Object类型,这两种声明方式没有什么用。
以上三种声明对象类型的方式,几乎没有什么用,就只剩下对象字面量的声明方式。对象字面量语法声明对象类型,声明的是对象的形状,对象包含哪些属性。这样声明对象类型的方式,叫作structurally typed(duck typing )。structurally typed(duck typing )是一种编程风格,它只关心对象有哪些属性,而不关心它名义上的类型。
当把一个对象字面量赋值给一个对象类型变量时,TS会执行非常严格的类型检查。对象字面量中的属性,既不能比对象类型中的定义的属性多,也不能比类型中的定义的属性少,必须一一对应,否则报错,这叫excess property check
let obj: {b: number} = { b: 123, /* Type '{ b: number; a: number; }' is not assignable to type '{ b: number; }'. Object literal may only specify known properties, and 'a' does not exist in type '{ b: number; }' */ a: 456 }
但这会带来问题,有的对象有某个属性,有的对象却没有这个属性,有的对象有很多属性,有的对象却没有这些属性,但它们都有共有的属性a,这样怎么声明,可使用?: 标示某个属性可有可无,也可以使用索引表达式 [key: number]: boolean。 key(可以取任意名字)表示对象的属性,是number类型,值是布尔类型。
let obj: { b: number c?: string [key: number]: boolean }
obj 必须有b属性,且是number类型。c属性可能有也可能没有,如果有c属性,它的值可以是undefined。如果还有其属性,它们必须是number类型,且属性值必须是布尔值。
obj = {b: 3}; obj = {b: 2, c:undefined}; obj = {b: 2, 10: false} obj = {10: false} // 报错,没有b属性。
函数类型
无论是函数声明,还是函数表达式,在写函数的过程中可以直接定义其参数类型和返回值。
function sum(a: number, b: number): number { return a + b; }
const substract = (a:number, b:number):number => a - b;
参数a,b都是number类型,参数列表() 后面跟 :number,表示函数的返回值类型,是number类型。如果TS能推断出函数的返回值类型,返回值类型可以省略。
function sum(a: number, b: number) { return a + b; }
当你把鼠标放到sum上时,可以看到TS已经推断出函数的返回值是number类型。如果参数类型也不写呢?
function sum(a, b) { return a + b; }
鼠标放到a上,a是any类型。当TypeScript不能推断一个变量的类型时,它就默认变量的类型为any。这叫隐式any,因为你没有写any这个关键字,变是却是any类型。由于any类型,容易引起问题,所以最好不要让TS做这种类型推断。如果参数确实是any类型,要显示地标示出来。配置项noImplicitAny 就是阻止TS做这种类型推断的。当在tsconfig.json文件中配置noImplicitAny: true时,如果你没有为变量标注类型,而是让TS推断出了它的类型是any时,TS就会报错。
function sum(a, b) {// Parameter 'a' implicitly has an 'any' type. return a + b; }
如果函数没有返回值呢?返回值类型是void
function print(a: string): void { console.log(a); }
当函数抛出错误呢,返回值是never类型,永远不会发生。never类型是所有类型的子类型,你可以把never类型赋值给任意其它类型。
function erorr(): never { throw new Error('Error'); }
有时函数参数是默认参数或可能传参,也可能不传参的参数,参数类型用?: 表示。
function request(url: string, method?: string){}
函数中还有一个特殊的存在this,函数调用方式不同,this指向就不同,这也是问题的所在,所以函数中有this时,最好声明this的期望类型。在TypeScript中,可以在参数列表中声明this,不过,this要作为第一个参数存在,其它参数放到它后面
function fancyDate(this: Date, name?: string) { return `${this.getDate()}/${this.getMonth()}/${this.getFullYear()}` } fancyDate.call(new Date)
当函数作为参数传递,或者作为函数返回值时,就不能用这种方式声明类型了,那就需要自定义类型了。
使用type自定义类型
使用type自定义类型,格式为: type 类型名称 = 类型。 以sum函数为例,它的类型是什么呢?两个number类型参数,返回一个number类型,类型就可以这么写
(a:number, b:number ) => number
很像箭头函数的语法,参数列表 => 返回值类型。再取个名字Sum,自定义类型就是
type Sum = (a:number, b:number ) => number;
Sum就是一个自定义的函数类型,只要一个函数接受两个number类型的参数,并返回number类型,这个函数就可以用Sum 标示。
let sub: Sum = (a, b) => a - b;
同时可标注参数或返回值
function runSum(fn: Sum, a:number, b: number) { return fn(a, b); } function returnSum(): Sum { return (a, b) => a + b ; } returnSum()(7, 8);
type Sum = (a:number, b:number ) => number; 是Sum类型的简写形式,更完整的形式如下
type Sum = { (a: number, b: number): number }
因为在JS中函数是一个对象,对象就会有属性,声明一个函数类型,也就是声明一个对象类型,就可以用对象字面量的方式声明它。
使用Type 来定义类型的时候,有两个重要概念,type union(联合类型),type insertection(交叉类型)。
type union(联合类型)
type union(联合类型):两个或多个类型使用 | 联合在一起,形成一个新的类型。可以把两个基本类型联合起来
type StrOrNum = string | number;
StrOrNum 或是string类型,或是number类型。使用这个联合类型时,只能使用string和number类型的共用的属性,两者很少共有属性。可以把两个对象类型联合起来
type Cat = {name: string, purrs: boolean} type Dog = {name: string, barks: boolean, wags: boolean} type CatOrDogOrBoth = Cat | Dog
当你看到一个类型是CatOrDogOrBoth的时候, 你想到了什么?保险起见,我只使用它们共有的属性name,这个类型肯定有一个name属性,其它属性有没有,不敢保证。反过来想,既然这个类型是CatOrDogOrBoth,我可以给它赋一个cat值,也可以给它赋一个Dog值,共有的值也可以赋给它。
let c:CatOrDogOrBoth = {name: 'cat', purrs: false } // Cat类型 let d:CatOrDogOrBoth = {name: 'dog', barks: false, wags: false } // dog类型 let cdBoth: CatOrDogOrBoth = { name: 'both', barks: true, purrs: true, wags: true}
类型联合时要注意,如果两个类型中有相同的属性,但属性却是不同的类型时,属性的两个类型也进行联合(union)
type Product = { id: number, name: string, price?: number }; type Person = { id: string, name: string, city: string }; // Product 和 Person 联合之后的结果 type UnionType = { id: number | string, name: string };
UnionType就是Product和Person类型进行联合后生成的新类型。在UnionType中,id属性的类型是联合类型 number | string ,就是因为id 在Product中是number 类型,但是在Person类型中是string类型. name属性在两个类型中都是string, 所以在新的联合类型中,name属性也是string
type insertection(交叉类型)type insertection(交叉类型):两个或多个类型使用 & 联合在一起,形成一个新的类型。
type strAndnum = string & number;
strAndnum,既是string,又是number,新的类型集合了string类型 和number类型的所有属性。正是由于把所有的属性都集合到了一起,你会发现,没有一个值既是string,又是number,所以这个类型没有意义。通常,&用于对象类型。
type Person = { id: string; name: string; city: string; } type Employee = { company: string; dept: string } type PersonAndEmpoyee = { id: string; name: string; city: string; company: string; dept: string }
PersonAndEmpoyee就是Person和Empoyee进行交叉生成的新的类型。你可以看到,type insertection(交叉类型) 就是把所有对象类型的属性全部组合到一起,允许你使用所有的属性,而不是共有类型。
当给一个交叉类型进行赋值时,值必须包含每一个类型中都定义的属性
let bob: Person & Employee = { id: "bsmith", name: "Bob", city: "London", company: "Acme Co", dept: "Sales" };
如果两个要交叉类型有相同的属性时,这时要注意。如果相同的属性,类型也相同,这没有问题。给Empoyee再声明一个id:string属性,Person & Employee 的结果还是一样
但是如果类型不同,这个属性的类型,是两个不同类型进行交叉,分别给Person和Employee 定义一个contact属性,类型不同
type Person = { id: string; contact: number; } type Employee = { id: string; contact: string; }
Person & Employee
type PersonAndEmpoyee = { id: string; contact: number & string; }
这时就出现问题了,没有一个值,它既是number 又是string. 解决办法,就是相同的属性不要使用原始类型,要使用对象。
type Person = { id: string; contact: {phone: number}; } type Employee = { id: string; contact: {name: string} } type PersonAndEmpoyee = { id: string; contact: {phone: number} & {name: string}; }