TypeScript学习
TypeScript给JS添加了类型系统,写JS时可以指定类型,类似静态语言。但添加了类型系统后,JS的运行时却不识别,需要编译。因此学习TS,一是学习类型系统,一是学习编译规则,编译器是现成的,typescript包提供了tsc编译器,只要告诉它规则,把TS转化成JS。新建ts-learning项目,npm init -y && npm install typescript -D。TS文件以.ts结尾,touch type.ts,
let str: string; // (变量名: 变量类型)明确定义str是string类型 let a = 3; // TS根据初始值会自动推断出a是数字类型。
TS类型系统除了明确定义外,还可以类型推断。在VS Code中,把鼠标移到变量a上时,TS已经推断出它是数字类型。
尽量让TS去推断类型,只有当TS不能正确推断类型时,才指定类型。代码写完了,提供编译规则(源文件在哪里,编译到哪里,怎么编译等),tsconfig.json配置文件,
{ "compilerOptions": { "rootDir": "./src", /* .ts文件的根目录 */ "outDir": "dist", /* 编译后的文件放到哪里 */ "target": "ES2015", /* 编译到目标语法*/ "module": "commonjs" /* 编译后的文件使用哪一种规范*/ } }
也可以npx tsc --init 生成配置文件,自己再手动修改不符合项目需求的地方。执行tsc进行编译,但需要注意,tsc type.ts(文件名),并不会使用根目录下的tsconfig.json文件,只有单独执行tsc,后面什么都不加,才会从项目根目录中寻找tsconfig.json, 然后根据它提供的规则进行编译。npx tsc,多出了dist目录,node dist/type.js运行程序,可以写一个命令
"scripts": { "start": "tsc && node dist/type.js" },
为什么要为JS添加类型呢? 因为JS语法太过灵活,各种隐式类型转换,容易引发bug,但又很难排查,比如 3+[]等于'3'。加了类型,就对值和操作进行了限定,在type.ts中写3+[],VS Code直接标红报错。鼠标移上去,也有报错的原因。VS Code内置了typescript包,它的右下角有{},点击,
5.2.2就是它内置的版本。typescript包有tsserver(TypeScript Language Service),编写TS代码时,编辑器会时时地与它进行通信,实实编译代码(编译到内存中),编译就会进行类型检查,如果不符合类型要求,就会报错,编辑器就会标红提示错误。执行tsc编译也是同样的道理,类型检查,报错。如果内置的版本和npm install的版本不一致,可以点击Select Version,选择 Use Workspace Version。有了类型,能尽早地发现问题,尽可能地把运行时发生的错误在编译时捕获,有利于写出安全运行的代码。
TS的基本类型和JS的基本类型一致,都是number, string, boolean, symbol, undefined和null。但undefined和null在默认情况下,也可以赋值给任意类型,
const x: number = null;
按理说,有了类型,null只能赋值给null类型,undefined只能赋给undefined类型。tsconfig.json配置strictNullChecks: true,禁止undefined和null赋值给其它类型,以上代码报错了。
复杂类型或引用类型,JS有Object类,但在TS中表现更为丰富,不仅要表示它是Object,还要表示它有哪些属性,是什么类型。
let obj: {a: number} = { a: 12 }
obj是一个对象,有个number类型的a属性。你可能见到过object, Object,{}来表示对象类型,比如 let obj: object ={a: 2},但obj.a就会报错,它仅表示obj是个对象,你什么也做不了。Object和{}更没有用,let obj: Object;可以给obj赋任何值。{a: number}声明的是对象的形状(结构),它包含哪些属性,只关心对象有哪些属性,也称为结构化类型。如果 {a: number} 用的地方比较多,可以抽取出来定义一个类型,使用type 类型名称 = 类型 自定义类型
type ANumProps = { a: number } let obj: ANumProps = {a: 12}
结构化类型有一个问题,就是它只关心有没有它定义的属性,多了的属性,它不管。let obj1={a: 1, b: 2};obj = obj1没有问题,这个无解,只能少用,就当它只有这么些属性。但把对象字面量赋值给对象类型变量时,TS则会严格检查,对象字面量中的属性,既不能多,也不能少,必须正好,否则报错,这叫excess property check,obj = {a: 1, b: 2} 就会报错。如果真是有时有b属性,有时没有b属性,可使用?: 标示某个属性或许有,也可以使用索引表达式 [key: 类型]: 类型。 key(可以取任意名字)表示对象的属性,[key: number]: boolean,表示属性是number类型,值是布尔类型。
let obj: {
a: number
b?: string
[key: number]: boolean
}
obj必须有a属性,且是number类型。b属性可能有也可能没有,如果有,它的值可以是undefined。如果还有其属性,它们必须是number类型,且属性值必须是布尔值。
对象类型的子类型数组,更为关心数组的元素是什么类型。数组的类型声明是元素的类型后面跟上[]
let arr: number[]; // arr数组中每一个元素都是number类型
数组还有一个子类叫元组类型,在[]中声明元素类型,直接决定了数组的长度和数组中的每一个元素的类型,所以元组就是一个元素类型固定且长度固定的数组
let turple: [number, string];
turple是2个元素的数组,第一个元素的类型是number,第二个元素的类型是string。
let arr: number[] = [2, 3, 4, 5];
let turple: [number, string] = [2, 'name'];
对象类型的子类型函数,更关心它的参数类型,参数个数和返回值。函数声明或函数表达式,可以直接定义参数类型和返回值。如果TS能推断出函数的返回值类型,返回值类型可以省略。
function sum(a: number, b: number): number { // 参数a,b都是number类型,参数列表()后面的:number表示返回值类型,是number类型。 return a + b; } function print(a: string): void { // 函数没有返回值, 返回值类型是void console.log(a); } function erorr(): never { // 函数抛出错误,返回never类型,永远不会发生。never类型是所有类型的子类型,可以把它赋值给任意其它类型。 throw new Error('Error'); } function request(url: string, method?: string){} // 参数有默认值,可传,可不传,参数类型用?:。 const substract = (a: number, b: number) => a - b; //没有写返回值类型
把鼠标放到substract上,
函数的类型是(a: number, b: number ) => number,=> 前面是参数列表,后面是返回值类型。也对,substract函数就是接受两个number类型参数,返回一个number类型的值。给这个类型取个名字,自定义了个函数类型
type CalNum = (a: number, b: number ) => number; /* 或 type CalNum = { (a: number, b: number): number } 因为在JS中函数是一个对象,声明一个函数,也是声明一个对象,可以用对象字面量来定义类型。 */ const substract: CalNum = (a, b) => a - b;
这是函数类型的第二种写法,更常用于函数是参数,或函数作为返回值。如果参数没有定义类型?
function sum(a, b) {
return a + b;
}
也没有报错,鼠标放到a上,a被推断成是any类型,这叫隐式any,因为没有写any,变量却是any类型。any类型,可以给它赋任何值,也可以把它赋值给任何类型,相当于TS不会对any类型的变量做检查,尽量不要用any类型,也不要让TS做这种类型推断。参数确实是any类型,要显示地标示出来。tsconfig.json配置noImplicitAny: true,如果没有为变量标注类型,让TS推断出了它的类型是any,TS会报错。any类型跳过了TS的类型检查,但有时确实不知道变量是什么类型,需要一个类型标注,用unknown类型。任何类型的值都可以赋给unknown类型,但是unknown类型的值只能赋给它本身和 any类型。如果要使用unknown类型,使用之前一定缩小它的类型范围(narrow type)。缩小类型范围的方法有很多,比如typeof
let b: unknown; b = 3; if (typeof b === 'string'){ console.log(b.toUpperCase()); }
联合类型:两个或多个类型使用 | 联合在一起,形成一个新的类型。
type StrOrNum = string | number; // StrOrNum 或是string类型,或是number类型。 type threeNumber = 1 | 2 | 3; // 只能取1,2,3。1,2,3成了类型,称为字面量类型 type Cat = {name: string, purrs: boolean} type Dog = {name: string, barks: boolean, wags: boolean} type 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}
也是因为类型的不确定性,联合类型的使用有点麻烦,只能直接使用联合类型中所有类型的共性,比如 c.name。如果想做其它操作,就要类型收窄。类型收窄,除了typeof,还有类型断言和类型守卫。类型断言是,直接告诉编译器值是什么类型,使用as。
(cdBoth as Cat).purrs
对象类型守卫,一个是可以使用in操作符来检查属性,一个是类型判断函数
function isDog(testObj: any): testObj is Dog { // 返回值是testObj is Dog,函数参数是什么类型 return testObj.barks !== undefined; } if(isDog(obj)){}
类型联合时要注意,如果两个类型中有相同的属性,但属性类型不同,属性的两个类型也进行联合(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 Person = { id: string; name: string; city: string; } type Employee = { company: string; dept: string } type PersonAndEmpoyee = Person & Employee /** * 相当于type PersonAndEmpoyee = { id: string; name: string; city: string; company: string; dept: string } 把所有交叉类型的属性全部放到一起 */
既是这个类型,又是那个类型,所以给一个交叉类型赋值时,值必须包含所有属性
let bob: Person & Employee = { id: "bsmith", name: "Bob", city: "London", company: "Acme Co", dept: "Sales" };
当要交叉类型有相同的属性,但类型不同时,这个属性的类型,也是不同类型进行交叉,分别给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}; }
如果相同的属性是函数时,函数的类型也要相交(merge)。
相当于函数的重载了,JS中函数重载不是太好用。
function getContact(field: string | number): any { return typeof field === "string" ? "Alice" : 6512346543; }
泛型:参数化的类型,声明类型时,可以带参数,类型也可以定制化,成立一个类型生产工厂。真正使用泛型时,传递真正类型参数,确定类型。
class DataCollection<T> { //类型 DataCollection带有参数T protected items: T[] = []; constructor(initialItems: T[]) { this.items.push(...initialItems); } filter(predicate: (target: T) => boolean) { return this.items.filter(item => predicate(item)); } } type Person = { id: number; phone: number; } const people: Person[] = [{id: 1, phone: 123}, {id: 2, phone: 456}] let data = new DataCollection<Person>([...people]); /** 使用泛型时,传递真正的类型Person,泛型中的T被替换成Person * 相当于 * class DataCollection { protected items: Person[] = []; constructor(initialItems: Person[]) { this.items.push(...initialItems); } filter(predicate: (target: Person) => boolean) { return this.items.filter(item => predicate(item)); } } */ let filteredProducts = data.filter(data => data.id > 1)
extends是限制传递给泛型类的参数的类型。class DataCollection<T extends Person>,使用DataCollection时,传递的类型参数必须是Person类或和Person结构相同的类(鸭式类型)。创建函数时使用泛型,接受不同的类型,创建不同类型的函数
function identity<T>(arg: T): T { return arg; }
使用identity时,传递字符串类型identity<string>("hello"); 就相当于创建了
function identity(arg: string): string { return arg; }
使用泛型对类型做了限制,输入和输出都是同一个类型。如果调用函数的时候,能通过参数推断出传递的类型,可以直接调用identity("hello"); TS中还可以是type声明类型
type MyEvent<T=HTMLElement> = { // 提供default 类型 target: T click(name: T): T } type funct = { // 函数类型的另一种声明方式 <T>(name: T): T }
编译选项
target指定编译到哪个JS版本,ES5, ES6/ES2015, ES2016等,同时也指定了能使用的JS功能。ES5,ES2015,ES2016版本除了规定语法外,还会新增功能,比如ES2015 新增Map,ES5中就没有Map,因此如果把target设为ES5,在TS中使用Map就会报错。tsc只是编译语法,把语法换个格式,比如把ES6箭头函数()=>null 转换成ES5普通函数function(){return null},但它不会新增功能。但有些新增功能是能polyfill的,只要提供polyfill,就能在低环境下使用。lib就是告诉ts,这个feature在运行时是有的,ts就不会报错了。当配置lib时,一定要提供对应的polyfill,否则代码部署后就会报错,在IE9下使用Map一定报错。这种错误是开发人员造成的,因为是我们告诉TS,Map是存在的。
"lib": ["es2015.collection"]
module指定编译后的文件使用哪种模块化方式。不同的环境,不同的格式。比如前端开发,ts文件通常会被webpack打包,webpack识别import和export,甚至是动态加载,所以module直接设置成esnext。古老的node就是CommonJs,现代的node就是esnext。moduleResolution: 解析查找模块的算法,选项node表示,使用和Node.js一样的解析算法,Node.js 怎么解析模块,Typescript就怎么解析模块,最新的配置应该是node16/nodenext。随着ts的升级,这俩个配置越来越复杂,官网https://www.typescriptlang.org/docs/handbook/modules/guides/choosing-compiler-options.html给了建议,如果使用构建工具,就用(I’m using a bundler)的配置
{ "compilerOptions": { // module-related 设置. // 必须配置 "module": "esnext", "moduleResolution": "bundler", "esModuleInterop": true, // 建议参照构建工具的官网进行配置 "customConditions": ["module"], // 建议 "noEmit": true, // or `emitDeclarationOnly` "allowImportingTsExtensions": true, "allowArbitraryExtensions": true, "verbatimModuleSyntax": true, // or `isolatedModules` } }
如果只是进行Node.js开发,
{ "compilerOptions": { // 必须配置 "module": "nodenext", // `"module": "nodenext"`: 意味着以下配置 // "moduleResolution": "nodenext", // "esModuleInterop": true, // "target": "esnext", // 建议配置 "verbatimModuleSyntax": true, } }
如果想要编译后的代码时esm格式,要么文件名使用.mjs,要么在package.json设置type为true。
esModuleInterop主要设置import引入CommonJS 模块的语法。CommonJs 导出API的方式有两种,exports.sum ={} 和module.exports=sum. 当以exports.sum的方式导出时,ts可以 import {sam} from, 命名方式引入,但当以module.exports=sum导出时,只能使用import * as sum from。配置esModuleInterop: true, 可以使用default方式导入,import sum from
extends选项,去继承另外一个配置文件,比如npm install @tsconfig/node16, 配置文件就可以写么写
{ “extends”: "@tsconfig/node16/tsconfig.json", "include": ["src/**/*"] }
此时仍然可以写compilierOptions, 它会和extends里面的配置项合并,
{ “extends”: "@tsconfig/node16/tsconfig.json", “compilerOptions”: {"outDir": "dist"} "include": ["src/**/*"] }
declaration: 为每一个typescript文件生成 .d.ts的类型文件。
JS项目中使用TS
首先设置tsconfig.json的allowJs为true,让tsc编译JS,但不进行类型检查。如果想要类型检查,可再设置checkJs为true或在文件开头 //@ts-check 或@ts-nocheck开启关闭类型检查。其次为js文件export出的API生成 .d.ts 类型声明文件,使用declare
export declare const Sizzle: SizzleStatic; export declare function findNodes(node: ts.Node): ts.Node[]
类型声明文件告诉tsc,如果在TS代码中看到Sizzle或findNodes(),不要紧张。在运行时,应用程序将包含具有这些类型的JS代码(使用包含 const Sizzle和 findNodes()的JS库)。此类声明称为环境声明 - 这就是您告诉编译器相关变量将在运行时存在的方式。类型声明文件告诉TS,这些东西(变量/函数等)已经在JS中存在(定义)了,只是现在描述给你。在类型声明中,top-level的值需要 declare关键字(declare let, declare function, declare class 等),top-level 的interface和type不用。
如果是自己写的js代码,完成可以改成ts,如果不行,先写一个类型声明文件,如果不写,TS先类型推断,推断不出来,整个JS模块就是any。在JS文件同级目录下写类型声明文件 .d.ts,比如js文件名是a.js,那就会查找a.d.ts,如下所示,
因为当在TS文件中引入JS文件时,TypeScript就是到同级目录中查找JS文件的类型定义文件。但如果TS文件引入的是第三方JS模块,如果已经有了类型声明,比如@type/react,就安装上,如果没有,你也可以不写,直接在import模块的地方 //@ts-ignore
如果要写如果要写一个类型声明,格式如下
declare module 'module-name' {
export type Mytype = number;
export type MyDefaultType = {a: string};
export let myExport: Mytype;
let myDefaultExport: MyDefaultType;
export default myDefaultExport
}
模块的名称('module-name') 要和import from的路径一致,就能告诉TS模块的API
import ModlueName from 'module-name'
ModlueName.a // string
放到项目的哪里都可以。如下
在index.ts中 import foo模块,types.d.ts中foo模块声明就会使用。这是由于TS查找第三方JS模块的类型声明的算法决定。它会先查找本地的类型声明文件有没有对模块的声明,如果有就用,如果没有,就看模块package.json中有没有定义types(指向类型声明文件),如果还是没有,就看node_modules中@types有没有模块声明,比如@types/react。还是没有,再查找js的同级目录,如果没有,就是any了。你可能看到过本地声明文件中,declare module module 名字,比如 declare module 'react',什么都没做,它只是告诉TS有一个模块,但没有类型信息。模块以及模块export的内容都是any。