Typescript进阶
基础知识
JavaScript 的核心特点就是灵活,但随着项目规模的增大,灵活反而增加开发者的心智负担。例如在代码中一个变量可以被赋予字符串、布尔、数字、甚至是函数,这样就充满了不确定性。而且这些不确定性可能需要在代码运行的时候才能被发现,所以我们需要类型的约束。
当然不可否认的是有了类型的加持多少会影响开发效率,但是可以让大型项目更加健壮
- Typescript 更像后端 JAVA,让
JS
可以开发大型企业应用; - TS 提供的类型系统可以帮助我们在写代码时提供丰富的语法提示;
- 在编写代码时会对代码进行类型检查从而避免很多线上错误;
越来越多的项目开始拥抱 TS 了,典型的 Vue3、Pinia、第三方工具库、后端 NodeJS 等。我们也经常为了让编辑器拥有更好的支持去编写**.d.ts 文件**。
什么是 Typescript
TypeScript 是一门编程语言,
TypeScript
是
Javascript
的超集(任何的
JS
代码都可以看成
TS
代码),同时
Typescript
扩展了
Javascript
语法添加了静态类型支持以及其他一些新特性。
TypeScript 代码最终会被编译成 JavaScript 代码,以在各种不同的运行环境中执行
环境配置
全局编译 TS 文件
全局安装
typescript
对
TS
进行编译
npminstall typescript -g
tsc --init# 生成tsconfig.json
tsc # 可以将ts文件编译成js文件
tsc --watch# 监控ts文件变化生成js文件
ts-node 执行 TS 文件
采用
vscode code runner插件
运行文件
npminstall ts-node -g
直接右键运行当前文件快速拿到执行结果
配置
rollup
开发环境
- 安装依赖
pnpminstall rollup typescript rollup-plugin-typescript2 @rollup/plugin-node-resolve rollup-plugin-serve -D
- 初始化
TS
配置文件npx tsc --init
rollup
配置操作rollup.config.mjs``````import ts from"rollup-plugin-typescript2";import{ nodeResolve }from"@rollup/plugin-node-resolve";import serve from"rollup-plugin-serve";import path from"path";import{ fileURLToPath }from"url";const __filename =fileURLToPath(import.meta.url);const __dirname = path.dirname(__filename);exportdefault{input:"src/index.ts",output:{format:"iife",file: path.resolve(__dirname,"dist/bundle.js"),sourcemap:true,},plugins:[nodeResolve({extensions:[".js",".ts"],}),ts({tsconfig: path.resolve(__dirname,"tsconfig.json"),}),serve({open:true,openPage:"/public/index.html",port:"3000",}),],};
package.json
配置"scripts":{"start":"rollup -c -w"}
我们可以通过
npm run start
启动服务来使用 typescript 啦~
常用插件
- Error Lens 提示错误插件
- TypeScript 内置配置 (code->首选项->settings)根据需要打开设置即可
基础类型
TS 中有很多类型:内置的类型 (DOM、Promise 等都在 typescript 模块中) 基础类型、高级类型、自定义类型。
TS 中冒号后面的都为类型标识,等号后面的都是值。
- ts 类型要考虑安全性,一切从安全角度上触发。
- ts 在使用的时候程序还没有运行
- ts 中有类型推导, 会自动根据赋予的值来返回类型,只有无法推到或者把某个值赋予给某个变量的时候我们需要添加类型。
- 通过export {}进行模块隔离
布尔、数字、字符串类型
let name:string="hs";// 全局也有name属性,需要采用模块化解决冲突问题let age:number=30;let handsome:boolean=true;
我们标识类型的时候 原始数据类型全部用小写的类型,如果描述实例类型则用大写类型(大写类型就是装箱类型,其中也包含拆箱类型),例如下面的大写:String
let s1:string="abc";let s2:string=newString("abc");// 不支持let s3: String =newString("abc");let s4: String ="abc";
什么是包装对象?
我们在使用原始数据类型时,调用原始数据类型上的方法,默认会将原始数据类型包装成对象类型。
数组
数组用于储存多个相同类型数据的集合。 TypeScript 中有两种方式来声明一个数组类型
let arr1:number[]=[1,2,3];let arr2:string[]=["1","2","3"];let arr3:(number|string)[]=[1,"2",3];// 联合类型let arr4:Array<number|string>=[1,"2",3];// 后面讲泛型的时候 详细说为什么可以这样写
元组类型
元组的特点就 固定长度 固定类型的一个数组(规定长度和存储的类型)
let tuple1:[string,number,boolean]=["hh",30,true];
tuple[3];// 长度为 "3" 的元组类型 "[string, number, boolean]" 在索引 "3" 处没有元素。let tuple2:[name:string, age:number, handsome?:boolean]=["hh",30,true];// 具名元祖
let tuple3:[string,number,boolean]=["hh",30,true];
tuple3.push("回龙观");// ✅ 像元组中增加数据,只能增加元组中存放的类型,但是为了安全依然无法取到新增的属性// tuple3.push({ address: "回龙观" }); // ❎let tuple4:readonly[string,number,boolean]=["hh",30,true];// 仅读元祖,不能修改,同时会禁用掉修改数组的相关方法
我要求媳妇有车有房 ,满足即可(底线) ,有可能我媳妇还有钱,but 这个钱不能花 ,因为不知道有没有。
枚举类型
枚举可以看做是自带类型的对象,枚举的值为数字时会自动根据第一个的值来递增 ,枚举中里面是数字的时候可以反举。
enumUSER_ROLE{USER,// 默认从0开始ADMIN,MANAGER,}// {0: "USER", 1: "ADMIN", 2: "MANAGER", USER: 0, ADMIN: 1, MANAGER: 2}
可以枚举,也可以反举
// 编译后的结果(function(USER_ROLE){USER_ROLE[(USER_ROLE["USER"]=0)]="USER";USER_ROLE[(USER_ROLE["ADMIN"]=1)]="ADMIN";USER_ROLE[(USER_ROLE["MANAGER"]=2)]="MANAGER";})(USER_ROLE||(USER_ROLE={}));
异构枚举
既有数字,也有字符串
enumUSER_ROLE{USER="user",ADMIN=1,MANAGER,// 2}
常量枚举
如果不需要对象,如果只是使用值,可以直接采用常量枚举,否则用普通枚举
constenumUSER_ROLE{USER,ADMIN,MANAGER,}console.log(USER_ROLE.USER);// console.log(0 /* USER */);
null 和 undefined
任何类型的子类型,如果
TSconfig
配置中
strictNullChecks
的值为 true,则不能把 null 和 undefined 赋给其他类型。
let u1:undefined=undefined;let n1:null=null;// 默认情况下 只能null给null , undefiend给undefiend
let name1:number|boolean;
name1 =null;
name1 =undefined;// 非严格模式
void 类型
只能接受 null,undefined。void 表示的是空 (通常在函数的返回值中里来用);undefiend 也是空,所以 undefiend 可以赋值给 void。严格模式下不能将 null 赋予给 void。
functionfn1(){}functionfn2(){return;}functionfn3():void{returnundefined;}
never 类型
任何类型的子类型,never 代表不会出现的值(这个类型不存在)。不能把其他类型赋值给 never。
functionfn():never{// throw new Error();while(true){}}let a:never=fn();// never只能赋予给neverlet b:number= a;// never是任何类型的子类型,可以赋值给任何类型
never 实现完整性保护
functionvalidate(type:never){}// 类型“boolean”的参数不能赋给类型“never”的参数。functiongetResult(strOrNumOrBool:string|number|boolean){if(typeof strOrNumOrBool ==="string"){return strOrNumOrBool.split("");}elseif(typeof strOrNumOrBool ==="number"){return strOrNumOrBool.toFixed(2);}// 能将类型“boolean”分配给类型“never”。validate(strOrNumOrBool);}
联合类型自动去除 never
let noNever:string|number|boolean|never=1;// never自动过滤
object 对象类型
object
表示非原始类型
let create = (obj: object) => {};
create({});
create([]);
create(function () {});
这里要注意不能使用大写的 Object 或 {} 作为类型,因为万物皆对象(涵盖了原始数据类型)。
object、Object、{} 的区别
object
非原始类型;Object
所有值都可以赋予给这个包装类型;大Object是类{}
字面量对象类型;
Symbol 类型
Symbol 表示独一无二
const s1 =Symbol("key");const s2 =Symbol("key");console.log(s1 == s2);// 此条件将始终返回 "false",因为类型 "typeof s11" 和 "typeof s12" 没有重叠
BigInt 类型
const num1 = Number.MAX_SAFE_INTEGER+1;const num2 = Number.MAX_SAFE_INTEGER+2;console.log(num1 == num2);// truelet max: bigint =BigInt(Number.MAX_SAFE_INTEGER);console.log(max +BigInt(1)=== max +BigInt(2));
number
类型和
bigInt
类型是不兼容的
any 类型
不进行类型检测,一旦写了 any 之后任何的校验都会失效。声明变量没有赋值时默认为 any 类型,写多了 any 就变成 AnyScript 了,当然有些场景下 any 是必要的。
let arr:any=["hh",true];
arr ="回龙观";
可以在 any 类型的变量上任意地进行操作,包括赋值、访问、方法调用等等,当然出了问题就要自己负责了。
变量类型推断
TypeScript 的类型推断是根据变量的初始化值来进行推断的。如果声明变量没有赋予值时默认变量是
any
类型。
let name;// 类型为any
name ="hswen";
name =30;
声明变量赋值时则以赋值类型为准
let name ="hswen";// name被推导为字符串类型
name =30;
联合类型
在使用联合类型时,没有赋值只能访问联合类型中共有的方法和属性。
let name:string|number;// 联合类型console.log(name.toString());// 公共方法
name =30;console.log(name.toFixed(2));// number方法
name ="hswen";console.log(name.toLowerCase());// 字符串方法
字面量联合类型
// 通常字面量类型与联合类型一同使用typeDirection="Up"|"Down"|"Left"|"Right";let direction: Direction ="Down";
可以用字面量当做类型,同时也表明只能采用这几个值(限定值)。类似枚举。
对象的联合类型
typewomen=|{
wealthy:true;
waste:string;}|{
wealthy:false;
morality:string;};let richWoman: women ={
wealthy:true,
waste:"不停的购物",
morality:"勤俭持家",// 对象类型的互斥};
可以实现对象中的属性互斥。
类型断言
将变量的已有类型更改为新指定的类型,默认只能断言成包含的某个类型。
- 非空断言
let ele: HTMLElement |null= document.getElementById("#app");console.log(ele?.style.color);// JS中链判断运算符ele!.style.color ="red";// TS中非空断言ele元素一定有值
- 可选链操作符?.
在访问对象的属性或方法时,先检查目标对象及其属性是否存在。- 空值合并操作符??
,当左侧的表达式结果为null
或undefined
时,会返回右侧的值。 - 类型断言
let name:string|number;(name!asnumber).toFixed(2);// 强制(<number>name!).toFixed(2);name asboolean;// 错误 类型 "string | number" 到类型 "boolean" 的转换可能是错误的
> 尽量使用第一种类型断言因为在 React 中第二种方式会被认为是>> jsx>
> 语法 - 双重断言
let name:string|boolean;name!asanyasstring;
> 尽量不要使用双重断言,会破坏原有类型关系,断言为 any 是因为 any 类型可以被赋值给其他类型。
函数类型
函数的类型就是描述了函数入参类型与函数返回值类型
函数的两种声明方式
- 通过 function 关键字来进行声明
functionsum(a:string, b:string):string{return a + b;}sum("a","b");
可以用来限制函数的参数和返回值类型
- 通过表达式方式声明
typeSum=(a1:string, b1:string)=>string;let sum:Sum=(a:string, b:string)=>{return a + b;};
可选参数
let sum =(a:string, b?:string):string=>{return a + b ||"";};let sum =(a:string, b:string="b"):string=>{return a + b;};sum("a");
可选参数必须在其他参数的最后面。
剩余参数
const sum =(...rest:string[]):string=>{return rest.reduce((memo, current)=>(memo += current),"");};sum("a","b","c","d");
this 类型
this 类型要进行声明
typeIThis=typeof obj;functiongetName(this: IThis, key:keyof IThis){returnthis[key];}const obj ={ name:"hh"};getName.call(obj,"name");
- typeof 获取对应的类型
- keyof 获取类型对应的所有 key 类型
函数的重载
重载一般是有限的操作
重载,指我们可以定义一些名称相同的方法,通过定义不同的输入参数来区分这些方法。TypeScript 中的重载是伪重载,只有一个具体实现,是类型的重载,而不是逻辑的重载
functiontoArray(value:number):number[];functiontoArray(value:string):string[];functiontoArray(value:number|string){if(typeof value =="string"){return value.split("");}else{return value
.toString().split("").map((item)=>Number(item));}}toArray(123);// 根据传入不同类型的数据 返回不同的结果toArray("123");
重载适合于已知有限数量类型的情况,可以对不同类型的参数做出不同的处理。
类
类由三部分组成:构造函数、属性(实例属性、原型属性)、方法(实例方法、原型方法、访问器)
TS 中定义类
classCircle{
x!:number;// 实例上的属性必须先声明
y!:number;constructor(x:number, y:number=0,...args:number[]){this.x = x;this.y = y;}}let p =newCircle(100);
实例上的属性需要先声明在使用,构造函数中的参数可以使用可选参数和剩余参数。
类中的修饰符
public
修饰符(谁都可以访问到)classAnimal{public name!:string;// 不写public默认也是公开的public age!:number;constructor(name:string, age:number){this.name = name;this.age = age;}}classCatextendsAnimal{constructor(name:string, age:number){super(name, age);console.log(this.name,this.age);}}let p =newCat("Tom",18);console.log(p.name, p.age);// 外层访问
> 我们可以通过参数属性来简化父类中的代码。classAnimal{constructor(public name:string,public age:number){this.name = name;this.age = age;}}
protected
修饰符 (自己和子类可以访问到)classAnimal{constructor(protected name:string,protected age:number){this.name = name;this.age = age;}}classCatextendsAnimal{constructor(name:string, age:number){super(name, age);console.log(this.name,this.age);}}let p =newCat("Tom",18);console.log(p.name, p.age);// 属性“name”受保护,只能在类“Animal”及其子类中访问。
private
修饰符 (除了自己都访问不到)classAnimal{constructor(private name:string,private age:number){this.name = name;this.age = age;}}classCatextendsAnimal{constructor(name:string, age:number){super(name, age);console.log(this.name,this.age);// 无法访问}}let p =newCat("Tom",18);console.log(p.name, p.age);// 无法访问console.log(tom['name'])// 可以访问私有属性,绕过ts检测
readonly
修饰符 (仅读修饰符)> reaonly 在构造函数中可以随意修改(初始化) 在其他的地方就不能再次修改了。classAnimal{constructor(publicreadonly name:string,public age:number){this.name ="init";this.age = age;}changeName(name:string){this.name = name;// 仅读属性只能在constructor中被赋值}}classCatextendsAnimal{constructor(name:string, age:number){super(name, age);}}let p =newCat("Tom",18);p.changeName("Jerry");
静态属性和方法
classAnimal{static type ="哺乳动物";// 静态属性staticgetName(){// 静态方法return"动物类";}private _name:string="Tom";getname(){// 属性访问器returnthis._name;}setname(name:string){this._name = name;}}let animal =newAnimal();console.log(animal.name);
静态属性和静态方法是可以被子类所继承的。
Super 属性
classAnimal{say(message:string){console.log(message);}staticgetType(){return"动物";}}classCatextendsAnimal{say(){// 原型方法中的super指代的是父类的原型super.say("猫猫叫");}staticgetType(){// 静态方法中的super指代的是父类returnsuper.getType();}}let cat =newCat();console.log(Cat.getType());
这里要注意子类重写父类的方法,类型需要兼容。
classAnimal{say(message:string):void{// 这里的void表示不关心返回值console.log(message);}}classCatextendsAnimal{say(message:string){super.say(message);}}let cat =newCat();
cat.say("我要吃鱼");
私有构造函数
classSingleton{privatestatic instance =newSingleton();privateconstructor(){/* 此类不能直接例化 */}publicstaticgetInstance(){return Singleton.instance;}}const instance1 = Singleton.getInstance();const instance2 = Singleton.getInstance();console.log(instance1 == instance2);
抽象类
抽象类描述了一个类中应当有哪些成员(属性、方法等),如果在父类中定义了抽象方法,那么子类必须要实现。
- 抽象类中不能声明静态的抽象成员
- 抽象类中可以包含具体的实现
- 抽象类不能被new
- 抽象类中可以创建抽象属性和方法,让子类来实现,但是静态方法、属性不可以
abstractclassAnimal{// abstract static type = '哺乳动物' // “static”修饰符不能与“abstract”修饰符一起使用。// 可以在父类中定义抽象方法,子类必须要实现abstracteat:()=>void;// 实例方法eatabstractplay():void;// 原型方法play// 提供的真实存在的方法drink(){return"喝水";}}classTomextendsAnimal{
eat!:()=>void;play(){}}
重载
classToArrayConverter{convert(value:number):number[];convert(value:string):string[];convert(value:number|string):number[]|string[]{if(typeof value ==="string"){return value.split("");}else{return value
.toString().split("").map((item)=>Number(item));}}}const converter =newToArrayConverter();const result1:number[]= converter.convert(123);const result2:string[]= converter.convert("123");
TS 中类型的使用
函数类型
函数的类型就是描述了函数入参类型与函数返回值类型
函数的两种声明方式
- 通过 function 关键字来进行声明
function sum(a: string, b: string): string { return a + b;}sum("a", "b");
可以用来限制函数的参数和返回值类型
- 通过表达式方式声明
type Sum = (a1: string, b1: string) => string;let sum: Sum = (a: string, b: string) => { return a + b;};
类型推断
TypeScript 拥有类型推导能力,根据用户的输入自动推导其类型。
赋值推断
赋值时推断,类型从右像左流动,会根据赋值推断出变量类型
let name = "hswen"; // string
let age = 30; // number
let handsome = true; // boolean
返回值推断
自动推断函数返回值类型
function sum(a: string, b: string) {
return a + b;
}
sum("a", "b"); // string
上下文类型
基于位置的类型推导,反方向的类型推导
函数从左到右进行推断
type Sum = (x: string, y: number) => string;
const sum: Sum = (a, b) => a + b; // a=> string b=> number
let result = sum("hswen", 30); // result=> string
type ICallback = (a: string, b: number, c: boolean) => void;
function fn(callback: ICallback) {
let result = callback("1", 1, true); // result -> void
}
// d类型无法正确推断,因为上下文类型是基于位置推断的
// 这里的void表示不关心具体类型
fn((a, b, c, d) => 100);
这里再次强调为什么 void 代表不关心?为什么这样设计呢?
[1, 2, 3].forEach((item) => item); // forEach回调没有返回值,但是用户确可以随意返回内容,you known?
接口
接口可以在面向对象编程中表示行为的抽象,也可以描述对象的形状。 接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。 (接口中不能含有具体的实现逻辑)
- 用来描述数据形状的 (对象、类、函数、混合类型)
- 接口中的内容都是抽象的 (不能有具体的实现)
函数接口参数
我们可以约束函数中的参数,但是类型无法复用。
const fullName =({
firstName,
lastName,}:{
firstName:string;
lastName:string;}):string=>{return firstName + lastName;};
我们可以通过接口进行描述
interfaceIFullName{
firstName:string;
lastName:string;}const fullName =({ firstName, lastName }: IFullName):string=>{return firstName + lastName;};
函数类型接口
interfaceIFullName{
firstName:string;
lastName:string;}interfaceIFn{(obj: IFullName):string;}const fullName:IFn=({ firstName, lastName })=>{return firstName + lastName;};
通过接口限制函数的参数类型和返回值类型。
type 与 interface 区别
一般场景下我们使用 interface 用来描述对象、类的结构。
使用类型别名来描述函数签名、联合类型、工具类型、 映射类型。
- type 可以用联合类型
type xx = string | number
, interface 不能用联合类型 - type 别名不能被扩展(继承),interface 可以被继承和实现
- type 不能重名, interface 可以重名(会合并)
- type 可以做循环和条件,interface 不行
- 函数类型一般采用type声明
其它场景下,两者可以替换使用,无伤大雅。
函数混合类型
interfaceICounter{():number;// 限制函数类型
count:0;// 限制函数上的属性}const fn:ICounter=()=>{// 这里需要使用const进行声明,可能是因为:防止fn被重新赋值,因为用let修改了值,可能属性就不存在了return fn.count++;};
fn.count =0;let counter: ICounter = fn;console.log(counter());console.log(counter());
对象接口
对象接口可以用来描述对象的形状结构
interfaceIVegetables{// 类型
color:string;
taste:string;
size:number;}let veg1: IVegetables ={// 定义
color:"red",
taste:"sweet",
size:10,
a:1,// 如何增添这个a属性呢?};
- 方案 1:直接采用断言的方式指定为当前赋值的类型
- 方案 2:在类型中通过
?
增添 a 属性为可选属性 - 方案 3:利用同名接口合并的特点
- 方案 4:通过接口继承的方式扩展属性
- 方案 5:通过任意接口来扩展
- 类型兼容性、交叉类型等
?标识的属性为可选属性,
readOnly
标识的属性则不能修改。多个同名的接口会自动合并
interfaceIVegetables{readonly color:string;
size:string;
taste:"sour"|"sweet";}interfaceIVegetables{
a?:number;}const tomato: IVegetables ={
color:"red",
size:"10",
taste:"sour",};
tomato.color ="green";// 仅读属性不能进行修改
任意属性、可索引接口
interfacePerson{
name:string;[key:string]:any;// 索引签名类型}let p: Person ={
name:"hswen",
age:30,[Symbol()]:"回龙观",};
任意属性可以对某一部分必填属性做限制,其余的可以随意增减。
interfaceIArr{[key:number]:any;}let p: IArr ={0:"1",1:"2",3:"3",};let arr: IArr =[1,"d","c"];
可索引接口可以用于标识数组
索引访问操作符
interfaceIPerson1{
name:string;
age:number;[key:string]:any;}// 访问接口中的类型需要使用[], 不能使用.typePropType1= IPerson1["name"];typePropType2= IPerson1[string];interfaceIPerson2{
name:string;
age:number;}typePropTypeUnion=keyof IPerson2;// name | agetypePropTypeValueUnion= IPerson2[PropTypeUnion];// string | number
类接口
这里先来强调一下抽象类和接口的区别,抽象类中可以包含具体方法实现,接口中不能包含实现。
interfaceSpeakable{
name:string;speak():void;}// 这里不区分是实例的方法还是原型的方法interfaceChineseSpeakable{// speakChinese:()=>voidspeakChinese():void;// 一般采用这种方式,这种方式不进行逆变检测}classSpeakimplementsSpeakable, ChineseSpeakable {
name!:string;speak(){}speakChinese(){}}
一个类可以实现多个接口,在类中必须实现接口中的方法和属性。
接口继承
interfaceSpeakable{speak():void;}interfaceSpeakChineseextendsSpeakable{speakChinese():void;}classSpeakimplementsSpeakChinese{speakChinese():void{thrownewError("Method not implemented.");}speak():void{thrownewError("Method not implemented.");}}
构造函数类型
interfaceClazz{new(name:string):any;}// type IClazz = new ()=> anyfunctioncreateClass(target: Clazz, name:string){returnnewtarget(name);// 传入的是一个构造函数}classAnimal{constructor(public name:string){this.name = name;}}let r =createClass(Animal,"Tom");
这里无法标识返回值类型。
interfaceClazz<T>{new(name:string):T;}functioncreateClass<T>(target: Clazz<T>, name:string):T{returnnewtarget(name);}classAnimal{constructor(public name:string){this.name = name;}}let r =createClass(Animal,"Tom");
new() 表示当前是一个构造函数类型,这里捎带使用了下泛型。 在使用
createClass
时动态传入类型。
泛型
泛型就是在使用的时候确定类型,泛型类似于函数的参数。泛型参数的名称通常我们使用大写的 T / K / U / V / M / O …这种形式。
指定函数参数类型
- 单个泛型
const getArray =<T>(times:number, val:T):T[]=>{let result:T[]=[];for(let i =0; i < times; i++){
result.push(val);}return result;};getArray(3,3);// 3 => T => number
- 多个泛型
functionswap<T,K>(tuple:[T,K]):[K,T]{return[tuple[1], tuple[0]];}console.log(swap(["hswen",30]));
函数标注的方式
- 类型别名
typeTArray=<T,K>(tuple:[T,K])=>[K,T];const swap: TArray =<T,K>(tuple:[T,K]):[K,T]=>{return[tuple[1], tuple[0]];};
- 接口
interfaceIArray{<T,K>(typle:[T,K]):[K,T];}const swap: IArray =<T,K>(tuple:[T,K]):[K,T]=>{return[tuple[1], tuple[0]];};
两种标注方式均可,但是对于函数而言我们通常采用类型别名的方式。
泛型使用的位置
实现一个数组循环函数
// type ICallback = <T>(item: T, index: number) => void; ❎错误写法,这样写意味着调用函数的时候确定泛型typeICallback<T>=(item:T, index:number)=>void;typeIForEach=<T>(arr:T[], callback: ICallback<T>)=>void;const forEach:IForEach=(arr, callback)=>{for(let i =0; i < arr.length; i++){callback(arr[i], i);// ts 类型检测 此时不会执行代码。}};forEach([1,2,"a","b"],function(item){console.log(item);});
泛型T 写在前面就是表示使用类型的时候传参,写到函数的前面意味着着调用函数的时候传递参数
默认泛型
在使用一些联合类型的时候,会使用泛型
type Union<T = string> = number | T;
const u1: Union = "abc";
const u2: Union<boolean> = true;
可以指定泛型的默认类型,让使用更方便。
泛型约束
使用
extends
关键字来约束传入的泛型参数必须符合要求。
A extends B
意味着 A 是 B 的子类型
'abc' extends string
'a' extends 'a' | 'b'
- …
案例 1:
functionhandle<Textendsstring|number>(input:T):T{return input;}
案例 2:
interfaceIWithLength{
length:number;}functiongetLen<Textends IWithLength>(val:T){return val.length;}getLen("hello");
案例 3:
const getVal =<T,KextendskeyofT>(obj:T, key:K):T[K]=>{return obj[key];};getVal({ name:"hh"},"name");
泛型约束经常也配合着条件类型来使用,后面讲到条件类型时在详细说明。
对象中的泛型
通过接口定义一个特定的响应类型结构
interfaceApiResponse<T=any>{
code:number;
data:T;
message?:string;}
调用接口时传入返回数据的结构类型
通泛型坑位,来占位置
interfaceLoginRes{// 登录接口的返回值
token:string;
roles:number[];}functiontoLogin(): ApiResponse<LoginRes>{return{
code:0,
data:{
token:"Bear token",
roles:[1,2],},};}
类中的泛型
创建实例时提供类型
classMyArray<T>{// T => number
arr:T[]=[];add(num:T){this.arr.push(num);}getMaxNum():T{let arr =this.arr;let max = arr[0];for(let i =1; i < arr.length; i++){let current = arr[i];
current > max ?(max = current):null;}return max;}}let myArr =newMyArray<number>();// 没有传递类型,默认类型为unknown
myArr.add(3);
myArr.add(1);
myArr.add(2);console.log(myArr.getMaxNum());
交叉类型
交叉类型(Intersection Types)是将多个类型合并为一个类型
- 联合类型的符号是|,类似按位或。只需要符合联合类型中的一个类型即可。 (并集)
- 交叉类型的符号是&,类似按位与。需同时满足类型。 (交集)
interfacePerson1{
handsome:string;}interfacePerson2{
high:string;}typeP1P2= Person1 & Person2;let p:P1P2={ handsome:"帅", high:"高"};
举例:我们提供两拨人,一拨人都很帅、另一拨人很高。我们希望找到他们的交叉部分 => 又高又帅的人。
interfacePerson1{
handsome:string;
address:{
pos:string;};}interfacePerson2{
high:string;
address:{
pos:number;};}typeP1P2= Person1 & Person2;// address 内部也会进行交叉类型typePOS=P1P2["address"]["pos"];// never = string & number
- 交叉类型
functionmixin<T,K>(a:T, b:K){return{...a,...b };}const x =mixin({ name:"hs", age:30},{ age:"20"});
这里返回值默认会被识别成交叉类型,但是如果两个对象中有相同属性类型不同,则默认推导会出现问题,后续我们再来解决这个问题。
unknown
unknown
类型,任何类型都可以赋值为
unknown
类型。 它是 any 类型对应的安全类型。any 叫做不检测了, unknown 要进行类型检测
不能访问 unknown 类型上的属性,不能作为函数、类来使用
// 类型检查后使用functionprocessInput(input:unknown){if(typeof input ==="string"){console.log(input.toUpperCase());}elseif(typeof input ==="number"){console.log(input.toFixed(2));}else{console.log(input);// unknown}}// 类型断言后使用let name:unknown="hswen";(name asstring).toUpperCase();
使用 unknown 类型需要进行类型检查或类型断言后再进行使用。
unknown 特性
- 联合类型中的
unknown``````typeUnionUnknown=unknown|null|string|number;
> 联合类型与>> unknown>
> 都是>> unknown>
> 类型 - 交叉类型中的
unknown``````unknown
表示类型未知,null
是一种具体的值,结果会受到null
的限制,最终结果会变成null
类型,而不是保持unknown
类型。typeinter=unknown&null;// nulltypeinter=any&null;// any
> 交叉类型与>> unknown>
> 都是其他类型 - keyof unknown 是 never
typekey=keyofunknown;// never// type key = keyof any; // string | number | symbol
条件类型
条件类型的语法类似于三元表达式
条件类型基本使用
可以使用
extends
关键字和三元表达式,实现条件判断。条件类型大部分场景是和泛型一起使用的
typeResStatusMessage<Sextendsnumber>=Sextends200|201|204?"success":"fail";typeMessage= ResStatusMessage<300>;// 传入要判断的类型
typeConditional<T,C>=TextendsC?true:false;typeR1= Conditional<"hswen",string>;// truetypeR2= Conditional<"hswen",number>;// false 条件也可以通过泛型传入
interfaceFish{
name:"鱼";}interfaceWater{
type:"水";}interfaceBird{
name:"鸟";}interfaceSky{
type:"天空";}typeCondition<T>=TextendsFish? Water : Sky;// 类型相同也可以使用extendslet con1: Condition<Fish>={ type:"水"};
多条件类
typeFormatReturnType<T>=Textendsstring// 可以编写多条件类型?string:Textendsnumber?number:never;functionsum<Textendsstring|number>(x:T, y:T): FormatReturnType<T>{// 泛型不能做数学运算return x +(y asany);}sum("abc","abc");// stringsum(123,123);// number
类型兼容性问题
extends
本质上是判断类型的兼容性,只需要兼容则条件即可成立
基本数据类型的兼容性
typeR1="abc"extendsstring?true:false;// truetypeR2=123extendsnumber?true:false;// truetypeR3=trueextendsboolean?true:false;// true// so~~~let r1:string="abc";let r2:number=123;let r3:boolean=true;
字面量类型可以赋予给原始数据类型。
联合类型的兼容性
在联合类型中,只需要符合其中一个类型即是兼容,从安全角度来看,就是你赋值的类型我这里支持。
typeR4="a"extends"a"|"b"|"c"?true:false;// truetypeR5=123extends123|456|789?true:false;// truetypeR6=stringextendsboolean|string|number?true:false;// so~~~let r4:"a"|"b"|"c"="a";let r5:123|456|789=123;let temp ="hello";let r6:boolean|string|number= temp;
联合类型中所有成员在另一个联合类型中都能找到就是兼容
原始类型与装箱类型兼容性
大写的就是装箱类型
typeR7=stringextendsString?true:false;// truetypeR8=numberextendsNumber?true:false;// truetypeR9= object extendsObject?true:false;// truetypeR10= String extendsObject?true:false;// true// so~~~let r7: String ="abc";let r8: Number =123;let r9: Object ={};let r10: Object =newString("abc");
原始类型可以赋予给装箱类型最终可以赋予给 Object 类型。
any 及 unknown
typeR11= Object extendsany?true:false;// truetypeR12= Object extendsunknown?true:false;// true// so~~~let tempObj: Object ={};let r11:any= tempObj;let r12:unknown= tempObj;
any 和 unkown 即为顶级类型。
其它类型的兼容性
- never 是任何类型的子类型,也就是最底端的类型
- null 和 undefiend 在严格模式下不能赋予给其他类型。undefined 可以赋予给 void 类型
typeR13=neverextends"abc"?true:false;// truetypeR14=undefinedextendsundefined?true:false;// truetypeR15=nullextendsnull?true:false;// truetypeR16=undefinedextendsvoid?true:false;// true
never 为最底端类型。
Never《 字面量《 字面量联合类 型| 字面量类型《 原始数据类型《 包装类型 《 Object <any|unnknown
类型层级
根据类型兼容性我们可以得出以下结论:
- never < 字面量类型
- 字面量类型 < 字面量类型的联合类型
- 原始类型 < 原始类型的联合类型
- 原始类型 < 装箱类型 < Object 类型
- Object < any | unknown
unknown & any 特殊情况
typeR17=unknownextends1?true:false;// 不能赋予给除unknown之外的类型typeR18=anyextends1?true:false;// booleantypeR19=anyextendsany?true:false;// 条件是 any,依然会进行判断
any可以分解成条件满足、和不满足两部分,则返回条件类型结果组成的联合类型。但是与
any
进行判断时依然会进行正常判断。
{} | object | Object 特殊情况
typeR20={}extendsobject?true:false;// truetypeR21={}extendsObject?true:false;// true// 鸭子类型检测,可以看出对象是基于{}扩展出来的typeR22= Object extends{}?true:false;// truetypeR23= object extends{}?true:false;// true// 以下两种情况均默认成立typeR24= Object extendsobject?true:false;// truetypeR25= object extendsObject?true:false;// true
条件类型与映射类型
条件类型分发
出现条件分发的场景
- 类型参数需要是一个联合类型。
- 类型参数需要通过泛型参数的方式传入
- 条件类型中的泛型参数是否完全裸露,只有裸类型才可以被分发。
typeCondition1= Fish | Bird extendsFish? Water : Sky;// skytypeCondition2<T>=TextendsFish? Water : Sky;typeR1= Condition2<Fish | Bird>;// water | sky
这里会用每一项依次进行分发,最终采用联合类型作为结果,等价于:
typec1= Condition2<Fish>;typec2= Condition2<Bird>;typec= c1 | c2;
禁用分发
默认情况下有些时候我们需要关闭这种分发能力,会造成判断不准确
// type unionAssets<T, U> = T extends U ? true : false;typeR1= unionAssets<1|2,1|2|3>;// true 看似正常typeR2= unionAssets<1|2,1>;// boolean (开启分发类型结果为boolean)// 禁用分发typeunionAssets<T,U>=[T]extends[U]?true:false;typeNoDistribute<T>=T&{};// 这种情况会返回一个新类型,从而阻止分发typeunionAssets<T,U>= NoDistribute<T>extendsU?true:false;
特殊问题
通过泛型传入的参数为 never,则会直接返回 never。
typeisNever1<T>=Textendsnever?true:false;typeisNever2<T>=[T]extends[never]?true:false;// 包裹后不在是nevertypeR4= isNever1<never>;// 返回nevertypeR5= isNever2<never>;// 返回true
内置条件类型
Extract
抽取类型(交集)
typeExtract<T,U>=TextendsU?T:never;typeMyExtract= Extract<"1"|"2"|"3","1"|"2">;
Exclude
排除类型(差集)
typeExclude<T,U>=TextendsU?never:T;typeMyExclude= Exclude<"1"|"2"|"3","1"|"2">;
补集如何实现呢?约束 U 是 T 的子集求出来的就是补集了。
typeComplement<T,UextendsT>=TextendsU?never:T;typeMyComplement= Complement<"1"|"2"|"3","1"|"2">;// 补集
NoNullable
非空检测
typeNonNullable<T>=Textendsnull|undefined?never:T;typeNonNullable<T>=T&{};// 保留联合类型中非空的值typeMyNone= NonNullable<"a"|null|undefined>;
infer 类型推断
TypeScript 中通过 infer(inference)关键字在条件类型中提取类型的某一部分信息。根据 infer 的位置不同,我们就能够获取到不同位置的类型。
基于 infer 的内置类型
使用 infer 需要先创造一个条件才可以
ReturnType
返回值类型functiongetUser(a:number, b:number){return{ name:"hswen", age:30};}typeReturnType<T>=Textends(...args:any)=>inferR?R:never;typeMyReturn= ReturnType<typeof getUser>;
Parameters
参数类型typeParameters<T>=Textends(...args:inferR)=>any?R:any;typeMyParams= Parameters<typeof getUser>;
ConstructorParameters
构造函数参数类型classPerson{constructor(name:string, age:number){}}typeConstructorParameters<T>=Textends{new(...args:inferR):any}?R:never;typeMyConstructor= ConstructorParameters<typeof Person>;
InstanceType
实例类型typeInstanceType<T>=Textends{new(...args:any):inferR}?R:any;typeMyInstance= InstanceType<typeof Person>;
内置类型的使用
function createInstance<Textendsnew(...args:any[])=>any>(
Ctor:T,...args: ConstructorParameters<T>): InstanceType<T>{returnnewCtor(...args);}classAnimal{constructor(public name:string){}}const animal =createInstance(Animal,"动物");
infer 实践
类型交换
typeSwap<T>=Textends[inferA,inferB]?[B,A]:T;typeSwapS1= Swap<["hh",30]>;// [30, "hh"]typeSwapS2= Swap<[1,2,3]>;// [1, 2, 3]
typeTailToHead<T>=Textends[inferA,...infer Args,inferB]?[B,A,...Args]:T;typeR100= TailToHead<["hh",30,"回龙观"]>;// ["回龙观", "hh", 30]
递归推断
typePromiseVal<T>=TextendsPromise<inferV>? PromiseVal<V>:T;typePromiseResult= PromiseVal<Promise<Promise<number>>>;// number
将数组类型转化为联合类型
typeElementOf<T>=TextendsArray<inferE>?E:never;typeTupleToUnion= ElementOf<[string,number,boolean]>;typeTupleToUnion=[string,number,boolean][number];
映射类型
所谓的映射类型,类似于 map 方法。核心就是基于键名映射到键值类型 (使用的是 in 关键字)
typeA1={ name:string};typeA2={ age:number};typeCompute<T>={// 映射类型 索引类型查询 索引类型访问[KinkeyofT]:T[K];};typeA1A2= Compute<A1&A2>;// {name:string,age:number}
Partial 转化可选属性
interfaceCompany{
num:number;}interfacePerson{
name:string;
age:string;
company: Company;}// type Partial<T> = { [K in keyof T]?: T[K] }; 实现原理typePartialPerson= Partial<Person>;
遍历所有的属性将属性设置为可选属性,但是无法实现深度转化!
typeDeepPartial<T>={[KinkeyofT]?:T[K]extendsobject? DeepPartial<T[K]>:T[K];};typeDeepPartialPerson= DeepPartial<Person>;
我们可以实现深度转化,如果值是对象继续深度转化。
Required
typePartialPerson= Partial<Person>;typeRequired<T>={[KinkeyofT]-?:T[K]};typeRequiredPerson= Required<PartialPerson>;
将所有的属性转化成必填属性
Readonly 转化仅读属性
typeReadonly<T>={readonly[KinkeyofT]:T[K]};typeReadonlyPerson= Readonly<PartialPerson>;
将所有属性变为仅读状态。
typeMutable<T>={-readonly[KinkeyofT]:T[K]};// 所有属性变成可变属性typeMutablePerson= Mutable<ReadonlyPerson>;
结构类型
Pick 挑选所需的属性
typePick<T,UextendskeyofT>={[PinU]:T[P]};typePickPerson= Pick<Person,"name"|"age">;
在已有类型中挑选所需属性。
Omit 忽略属性
let person ={
name:"hs",
age:11,
address:"回龙观",};typeOmit<T,KextendskeyofT>= Pick<T, Exclude<keyofT,K>>;typeOmitAddress= Omit<typeof person,"address">;
忽略 person 中的 address 属性 (先排除掉不需要的 key,在通过 key 选出需要的属性)
functionmixin<T,K>(a:T, b:K): Omit<T,keyofK>&K{return{...a,...b };}const x =mixin({ name:"hs", age:30},{ age:"20"});
Record 记录类型
只想要 key-> value 的格式可以采用 Record 类型
record 通常用来代替 object 。
typeRecord<Kextendskeyofany,T>={[PinK]:T};let person: Record<string,any>={ name:"hswen", age:30};
实现 map 方法,我们经常用 record 类型表示映射类型
functionmap<Textendskeyofany,K,U>(
obj: Record<T,K>,callback:(item:K, key:T)=>U){let result ={}as Record<T,U>;for(let key in obj){
result[key]=callback(obj[key], key);}return result;}const r =map({ name:"hswen", age:30},(item, key)=>{return item;});
兼容性
TypeScript 的类型系统特性:结构化类型系统(鸭子类型检测),TypeScript 比较两个类型不是通过类型的名称,而是比较这两个类型上的属性与方法。
基本数据类型的兼容性
你要的我有就可以
let obj:{toString():string;};let str:string="hh";
obj = str;// 字符串中具备toString()方法,所以可以进行兼容
string 可以看成基于对象 toString 进行扩展的子集,(从安全度考虑,因为在最后使用 obj 时只允许调用 toString 方法)
接口兼容性
interfaceIAnimal{
name:string;
age:number;}interfaceIPerson{
name:string;
age:number;
address:string;}let animal: IAnimal;let person: IPerson ={
name:"hh",
age:30,
address:"回龙观",};
animal = person;
接口的兼容性,只要满足接口中所需要的类型即可!
函数的兼容性
函数的兼容性主要是比较参数和返回值
- 参数
letsum1=(a:string, b:string)=> a + b;letsum2=(a:string)=> a;sum1 = sum2;
赋值函数的参数要少于等于被赋值的函数,与对象相反,例如:typeFunc<T>=(item:T, index:number)=>void;functionforEach<T>(arr:T[], cb: Func<T>){for(let i =0; i < arr.length; i++){cb(arr[i], i);}}forEach([1,2,3],(item)=>{console.log(item);});
- 返回值
typesum1=()=>string|number;typesum2=()=>string;let fn1: sum1;let fn2!: sum2;fn1 = fn2;
类的兼容性
classClassA{
name:string="hh";
age:number=30;}classClassB{
name:string="hh";
age:number=30;
address:string="回龙观";}let parent: ClassA =newClassB();// 可以看成ClassB是继承于ClassA的子类,子类赋予给父类兼容
这里要注意的是,只要有 private 或者 protected 关键字类型就会不一致
classClassA{private name:string="hh";
age:number=30;}classClassB{private name:string="hh";
age:number=30;}let clazz: ClassA =newClassB();// 不能将类型“ClassB”分配给类型“ClassA”。这也做到了模拟标称类型系统
结构化类型导致的问题
typeBTC=number;// 无法区分两个类型typeUSDT=number;let btc:BTC=1000;let usdt:USDT=1000;// 要求传入btcfunctiongetCount(count:BTC){return count asBTC;}let count =getCount(usdt);// 实际传入usdt
typeNominal<T,Uextendsstring>=T&{ __tag:U};typeBTC= Nominal<number,"btc">;typeUSDT= Nominal<number,"usdt">;// 标称类型let btc:BTC=1000asBTC;let usdt:USDT=1000asUSDT;functiongetCount(count:BTC){// 获取BTC的数量return count;}let count =getCount(usdt);// 报错:无法传入usdt
函数的逆变与协变
函数的参数是逆变的,返回值是协变的 (在非严格模式下 StrictFunctionTypes:false 函数的参数是双向协变的)
classParent{house(){}}classChildextendsParent{car(){}}classGrandsonextendsChild{sleep(){}}functionfn(callback:(instance: Child)=> Child){// 在使用此回调方法时可以传递 自己、或者子类型callback(newChild());let ins =callback(newGrandson());// 如果传递的是子类型,在使用的时候无法使用多出来的属性// ins是Child类型,我可以将Grandson类型传入。用的时候我只会调用Child类型的方法。因为安全所以兼容}fn((instance: Parent)=>{// instance.sleep() 这个不安全。因为如果传递的是Child 他不具备。// 但是如果这里标识Parent 是可以的。因为调用instance.house() 是安全的。returnnewGrandson();});
通过这个案例可以说明,函数签名类型中参数是逆变的,返回值可以返回子类型所以称之为协变的。
随着某一个量的变化而变化一致的即称为协变,而变化相反的即称为逆变。但是参数逆变也会带来一些问题。
传递的函数(传父(参数是逆变的)返子(返回值是协变的))
由此可得:
typeArg<T>=(arg:T)=>void;typeReturn<T>=(arg:any)=>T;typeArgReturn= Arg<Parent>extendsArg<Child>?true:false;// 基于函数参数的逆变typeReturnReturn= Return<Grandson>extendsReturn<Child>?true:false;// 返回值是协变的
逆变带来的问题:
interfaceArray<T>{// concat: (...args: T[]) => T[]; // 严格检参数测逆 Child 无法 赋予给 Parentconcat(...args:T[]):T[];// 不进行参数逆变检测[key:number]:T;}let parentArr!:Array<Parent>;let childArr!:Array<Child>;
parentArr = childArr;// 子应该可以赋予给父的~~~
泛型的兼容性
interfaceIT<T>{}let obj1:IT<string>;let obj2!:IT<number>;
obj1 = obj2;
枚举的兼容性
enumUSER1{
role =1,}enumUSER2{
role =1,}let user1!:USER1;let user2!:USER2;
user1 = user2;// 错误语法
不同的枚举类型不兼容。
类型保护
通过判断、识别所执行的代码块,自动识别变量属性和方法。将类型范围缩小。
typeof类型保护
function double(val: number | string) {
if (typeof val === "number") {
val.toFixed();
} else {
val.charAt(0);
}
}
instanceof类型保护
class Cat {}
class Dog {}
const getInstance = (clazz: { new (): Cat | Dog }) => {
return new clazz();
};
let r = getInstance(Cat);
if (r instanceof Cat) {
r;
} else {
r;
}
in类型保护
interface Fish {
swiming: string;
}
interface Bird {
fly: string;
leg: number;
}
function getType(animal: Fish | Bird) {
if ("swiming" in animal) {
animal; // Fish
} else {
animal; // Bird
}
}
可辨识联合类型
interface WarningButton {
class: "warning";
}
interface DangerButton {
class: "danger";
}
function createButton(button: WarningButton | DangerButton) {
if (button.class == "warning") {
button; // WarningButton
} else {
button; // DangerButton
}
}
// -----------类型中有独一无二的特性---------------
function ensureArray<T>(input: T | T[]): T[] {
return Array.isArray(input) ? input : [input];
}
null 保护
const addPrefix = (num?: number) => {
num = num || 1.1;
function prefix(fix: string) {
return fix + num?.toFixed();
}
return prefix("$");
};
console.log(addPrefix());
这里要注意的是 ts 无法检测内部函数变量类型。
自定义类型保护
interface Fish {
swiming: string;
}
interface Bird {
fly: string;
leg: number;
}
function isBird(animal: Fish | Bird): animal is Bird {
return "swiming" in animal;
}
function getAniaml(animal: Fish | Bird) {
if (isBird(animal)) {
animal;
} else {
animal;
}
}
自定义类型
内置类型可以分为以下几种类别:
- Partial、Required、Readonly 起到修饰的作用
- Pick Omit 处理数据结构
- Exclude、Extract 处理集合类型
- Parameters ReturnType 等 模式匹配类型
部分属性可选(修饰类型)
// 解题思路:将对应的属性挑选出来变为可选项 + 忽略掉对应的属性
type PartialPropsOptional<T extends object, K extends keyof T> = Partial<
Pick<T, K>
> &
Omit<T, K>;
interface Person {
name: string;
age: number;
address: string;
}
type Compute<T> = {
[K in keyof T]: T[K];
};
type t1 = Compute<PartialPropsOptional<Person, "age" | "address">>;
根据值类型(挑选/忽略)对象类型的属性 (结构类型)
// 解题思路:先找出类型相等的key,在通过Pick/Omit进行筛选
// 1)判断两个类型是否相等
type IsEqual<T, U, Success, Fail> = [T] extends [U]
? [U] extends [T]
? Success
: Fail
: Fail;
// 2) 如果相等,则返回对应的key。再取其联合类型
type ExtractKeysByValueType<T extends object, U> = {
[K in keyof T]: IsEqual<T[K], U, K, never>;
}[keyof T];
// 3) 通过联合类型挑选出所需的类型
type PickKeysByValue<T extends object, U> = Pick<
T,
ExtractKeysByValueType<T, U>
>;
type t2 = PickKeysByValue<Person, string>; // {name:string,address:string}
// 在来实现Omit:编写Omit逻辑应到正好相反
type ExtractKeysByValueType<T extends object, U, O = false> = {
[K in keyof T]: IsEqual<
T[K],
U,
IsEqual<O, true, never, K>, // 是Omit 则为never
IsEqual<O, true, K, never> // 不是Omit 就返回key
>;
}[keyof T];
type OmitKeysByValue<T extends object, U> = Pick<
T,
ExtractKeysByValueType<T, U, true> // 增加类型来判断是否是Omit
>;
type t3 = OmitKeysByValue<Person, string>;
// 重映射实现
type PickKeysByValue<T extends object, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
子类型互斥(集合类型)
interface Man1 {
fortune: string;
}
interface Man2 {
funny: string;
}
interface Man3 {
foreign: string;
}
// type ManType = Man1 | Man2 | Man3; // 我希望MainType只能是其中的一种类型
// let man: ManType = {
// fortune: "富有",
// funny: "风趣",
// foreign: "洋派",
// };
// 1)将对象的差集标记为never
type DiscardType<T, U> = { [K in Exclude<keyof T, keyof U>]?: never };
// 2) 差集(never) + 另一半
// (man1 - man2) 这里的属性标记为never + man2
// (man2 - man1) 这里的属性标记为never + man1
type OrType<T, U> = (DiscardType<T, U> & U) | (DiscardType<U, T> & T);
// type ManType = OrType<Man1, Man2>;
type ManType = OrType<Man1, OrType<Man2, Man3>>;
对象的交、差、并、补 (集合类型)
type A = {
name: string;
age: number;
address: string;
};
type B = {
name: string;
male: boolean;
address: number;
};
交集
type ObjectInter<T extends object, U extends object> = Pick<
T,
Extract<keyof T, keyof U>
>;
差集
type ObjectDiff<T extends object, U extends object> = Pick<
T,
Exclude<keyof T, keyof U>
>;
补集
// T多U少
type ObjectComp<T extends U, U extends object> = Pick<
T,
Exclude<keyof T, keyof U>
>;
重写
以后面的类型为准(取交集)在加上以前比现在多的类型。
// 取出覆盖的类型 + 加上差集
type Overwrite<T extends object, U extends object> = ObjectInter<U, T> &
ObjectDiff<T, U>;
模式匹配类型
// 推断函数类型中参数的最后一个参数类型
type LastParameter<T extends (...args: any[]) => any> = T extends (
...arg: infer P
) => any
? P extends [...any, infer L]
? L
: never
: never;
借助 Parameters 类型简化
type LastParameter<T extends (...args: any[]) => any> = Parameters<T> extends [
...any,
infer Q
]
? Q
: never;
模块及命名空间使用
模块和命名空间
默认情况下 ,我们编写的代码处于全局命名空间中
模块
文件模块: 如果在你的 TypeScript 文件的根级别位置含有 import 或者 export,那么它会在这个文件中创建一个本地的作用域。
// a.ts导出exportdefault"hh";// index.ts导入import name from"./a";
ESM 可以打包成
Commonjs
规范以及
AMD
规范,但是
commonjs
规范无法打包成
AMD
规范。
**如果一个模块是用
commonjs
规范来编写的,那么也无法采用 ES 模块方式来导入**
TS 模块语法
// a.ts导出export="hh";// index.ts导入import name =require("./a");// 也可以采用ES的方式导入,同时也可以打包成Commonjs或者AMD模块
命名空间
命名空间可以用于组织代码,避免文件内命名冲突(内部模块)。想要被外界使用也可以通过 export 导出命名空间。
- 命名空间的使用
// a.ts导出exportnamespace Zoo {exportclassDog{eat(){console.log("zoo dog");}}}exportnamespace Home {exportclassDog{eat(){console.log("home dog");}}}// index.ts导入import{ Zoo, Home }from"./a";let dog_of_zoo =newZoo.Dog();
dog_of_zoo.eat();let dog_of_home =newHome.Dog();
dog_of_home.eat();
- 命名空间嵌套使用
exportnamespace Earth {exportnamespace Contry {exportclassChina{}exportclassAmerica{}}}
Earth.Contry.China;
Earth.Contry.America;
命名空间中导出的变量可以通过命名空间使用。
命名空间合并
同名的命名空间可以自动合并, 如果命名空间散落到多个文件中想要被合并,可以采用后面要学的三斜线指令。
exportnamespace Zoo {exportclassDog{eat(){console.log("zoo dog");}}}exportnamespace Zoo {exportclassMonkey{eat(){console.log("zoo monkey");}}}
命名空间也可用于:扩展类、扩展方法、扩展枚举类型。
classA{static b ="hello b";}namespaceA{exportlet a ="hello a";}functioncounter():number{return counter.count++;}namespace counter {exportlet count =0;}enumROLE{
user =0,}namespaceROLE{exportlet admin =1;}
类型声明
声明全局变量
普通类型声明
declarelet age:number;declarefunctionsum(a:string, b:string):void;declareclassAnimal{}declareconstenum Seaons {
Spring,
Summer,
Autumn,
Winter,}declareinterfacePerson{
name:string;
age:number;}
一般情况下,我们会将 declare 声明的内容放置到类型声明文件中即
.d.ts
中,这样不会影响核心代码,并且统一管理。默认项目编译时会查找所有以
.d.ts
结尾的文件。
练习: 声明 jQuery 类型
jquery 通过外部 CDN 方式引入,想在代码中直接使用
interfaceJQuery{height(num?:number):this;width(num?:number):this;extend(obj: object):this;}// $(".box").height(100).width(100);// $.fn.extend({});
声明模块
// declare.d.tsdeclaremodule"mitt"{typeType=string|symbol;typeListener=(...args:any[])=>void;conston:(type: Type, listener: Listener)=>this;constemit:(type: Type,...args:any[])=>boolean;constoff:(type: Type, listener: Listener)=> Listener;}declaremodule"*.jpg"{const str:string;exportdefault str;}// index.tsimport mitt from"mitt";importtype{ Listener }from"mitt";// 仅导入类型import url from"a.jpg";let listener:Listener=function(data){console.log(data);};
mitt.on("data", listener);
mitt.emit("data","this is data");
mitt.off("data", listener);
第三方声明文件
@types 是一个约定的前缀,所有的第三方声明的类型库都会带有这样的前缀
npm install @types/jquery -S
当使用 jquery 时默认会查找
node_modules/@types/jquery/index.d.ts
文件
查找规范
- node_modules/jquery/package.json 中的 types 字段
- node_modules/jquery/index.d.ts
- node_modules/@types/jquery/index.d.ts
自己编写的声明文件放到目录中
@types/lodash
// lodash.d.ts/// <reference path="./lodash_a.d.ts" />export= _;// 将_当做模块导出exportasnamespace _;// 将这个模块作为全局变量使用,不需要导入(在不是作用域的文件中可以直接使用,umd 模块)declarenamespace _ {functiona():void;functionb():void;functionc():void;}
// lodash_a.d.tsimport _ =require("./lodash");declaremodule"./lodash"{// 对模块进行扩展functionx():void;functiony():void;functionz():void;}
namespace
表示一个全局变量包含很多子属性 , 命名空间内部不需要使用 declare 声明属性或方法
/// <reference path="./lodash_a.d.ts" />export= _;exportasnamespace _;declareconst _: _.ILodash;// 通过接口的方式导出declarenamespace _ {interfaceILodash{// 将模块内的属性全部放到接口中a():void;b():void;c():void;}}
import _ =require("./lodash");declaremodule"./lodash"{interfaceILodash{// 采用接口合并的特性进行扩展x():void;y():void;z():void;}}
三斜线指令
三斜线指令就是声明文件中的导入语句,用于声明当前的文件依赖的其他类型声明。
三斜线指令必须被放置在文件的顶部才有效
/// <reference path="./lodash_a.d.ts" /> // 依赖的某个声明/// <reference types="node" /> // 依赖的某个包/// <reference lib="dom" /> // 依赖的内置声明
我们一般只使用第一种方式,来进行声明的整合。
扩展全局变量类型
可以直接使用接口对已有类型进行扩展
interfaceString{double():string;}
String.prototype.double=function(){return(thisasstring)+this;};
interfaceWindow{
mynane:string;}console.log(window.mynane);
模块内全局扩展
declare global {interfaceString{double():string;}interfaceWindow{
myname:string;}}
声明全局表示对全局进行扩展。
TS 注释
@ts-ignore
忽略下一行的检测,不管是否有错误。
// @ts-ignorelet name:string="30";
@ts-expect-error
下一行代码真的存在错误时才能被使用。
// @ts-expect-errorconst age:number=30;
ts-nocheck
忽略整个文件的类型检测
// @ts-nocheckconst age:number="30";const name:string=30;
ts-check
用于为 JavaScript 文件进行类型检查 (需要配合 JSDoc )
// @ts-check/**
@param {string} a
@param {string} b
@returns {string}
*/functiongetType(a, b){return a + b;}getType("1","2");/** @type {string} */const age =30;
类型体操
基于字符串
CapitalizeString
首字母大写
// 默认情况下,同时进行推断,左边只有一个字母exporttypeCapitalizeString<T>=Textends`${inferL}${inferR}`?`${Capitalize<L>}${R}`// 左边大写 + 右边剩下的:T;// 不是字符串则直接返回// ---------------------------------typea1= CapitalizeString<"handler">;// Handlertypea2= CapitalizeString<"parent">;// Parenttypea3= CapitalizeString<233>;// 233
FirstChar
获取字符串字面量中的第一个字符
export type FirstChar<T> = T extends `${infer L}${infer R}` ? L : never;
// ---------------------------------
type A = FirstChar<"BFE">; // 'B'
type B = FirstChar<"dev">; // 'd'
type C = FirstChar<"">; // never
LastChar
获取字符串字面量中的最后一个字符
// 拆分左右两边类型,将右边递归拆分,通过泛型保留拆分后的结果
export type LastChar<T, F = never> = T extends `${infer L}${infer R}`
? LastChar<R, L> // 递归拆分右侧内容,L为上一次的左侧,最后不能拆分则返回L,L就位最后一个字符
: F;
// ---------------------------------
type A = LastChar<"BFE">; // 'E'
type B = LastChar<"dev">; // 'v'
type C = LastChar<"">; // never
StringToTuple
字符串转换为元组类型
export type StringToTuple<
T,
F extends any[] = []
> = T extends `${infer L}${infer R}` ? StringToTuple<R, [...F, L]> : F;
type A = StringToTuple<"BFE.dev">; // ['B', 'F', 'E', '.', 'd', 'e','v']
type B = StringToTuple<"">; // []
TupleToString
将字符串类型的元素转换为字符串字面量类型
// 拆分左右两边类型,将右边递归拆分,通过泛型保留拆分后的结果.
export type TupleToString<T, F extends string = ""> = T extends [
infer L,
...infer R
]
? TupleToString<R, `${F}${L & string}`> // 递归数组右侧的部分,每次拿到的左侧结果累加
: F;
// ---------------------------------
type A = TupleToString<["a", "b", "c"]>; // 'abc'
type B = TupleToString<["a"]>; // 'a'
type C = TupleToString<[]>; // ''
RepeatString
复制字符 T 为字符串类型,长度为 C
export type RepeatString<
T extends string, // 要循环的字符串
C, // 循环的次数
A extends any[] = [], // 采用数组记录循环的次数
F extends string = "" // 最终结果
// 如果满足长度返回最终结果,不满足则累加数组长度,并且拼接最终结果
> = C extends A["length"] ? F : RepeatString<T, C, [...A, T], `${F}${T}`>;
// ---------------------------------
type A = RepeatString<"a", 3>; // 'aaa'
type B = RepeatString<"a", 0>; // ''
SplitString
将字符串字面量类型按照指定字符,分割为元组。无法分割则返回原字符串字面量
export type SplitString<
T, // 要拆分的内容
S extends string, // 分隔符
A extends any[] = [] // 存放拆分后的结果
> = T extends `${infer L}${S}${infer R}`
? SplitString<R, S, [...A, L]> // 递归拆分右边,并且将左边放到数组中
: [...A, T]; // 不包含则直接将T 放到数组中
// ---------------------------------
type A1 = SplitString<"handle-open-flag", "-">; // ["handle", "open", "flag"]
type A2 = SplitString<"open-flag", "-">; // ["open", "flag"]
type A3 = SplitString<"handle.open.flag", ".">; // ["handle", "open", "flag"]
type A4 = SplitString<"open.flag", ".">; // ["open", "flag"]
type A5 = SplitString<"open.flag", "-">; // ["open.flag"]
LengthOfString
计算字符串字面量类型的长度
export type LengthOfString<
T,
A extends any[] = [] // 用于计算字符串的长度
> = T extends `${infer L}${infer R}`
? LengthOfString<R, [...A, L]>
: A["length"];
// ---------------------------------
type A = LengthOfString<"BFE.dev">; // 7
type B = LengthOfString<"">; // 0
KebabCase
驼峰命名转横杠命名
type RemoveFirst<T> = T extends `-${infer R}` ? R : T; // 删除首字母是-的
export type KebabCase<
T,
F extends string = ""
> = T extends `${infer L}${infer R}`
? // 看当前字母是否是大写,如果是 则转化成 -小写 H -> -h
KebabCase<R, `${F}${Capitalize<L> extends L ? `-${Lowercase<L>}` : L}`>
: RemoveFirst<F>;
// ---------------------------------
type a1 = KebabCase<"HandleOpenFlag">; // handle-open-flag
type a2 = KebabCase<"OpenFlag">; // open-flag
CamelCase
横杠命名转化为驼峰命名
type CamelCase<
T extends string,
S extends string = ""
> = T extends `${infer L}-${infer R1}${infer R2}` // 匹配 xx-x => xxX
? CamelCase<R2, `${S}${L}${Uppercase<R1>}`> // 累加-左边
: Capitalize<`${S}${T}`>;
// ---------------------------------
type a1 = CamelCase<"handle-open-flag">; // HandleOpenFlag
type a2 = CamelCase<"open-flag">; // OpenFlag
ObjectAccessPaths
得到对象中的值访问字符串
type RemoveFirst<T> = T extends `.${infer L}` ? L : T;
export type ObjectAccessPaths<
T,
F extends string = "",
K = keyof T
> = K extends keyof T // 产生一次分发操作
? T[K] extends object // 不能T[K]联合类型会出现never
? ObjectAccessPaths<T[K], `${F}.${K & string}`>
: RemoveFirst<`${F}.${K & string}`>
: never;
// ---------------------------------
function createI18n<Schema>(
schema: Schema
): (path: ObjectAccessPaths<Schema>) => void {
return (path) => {};
}
const i18n = createI18n({
home: {
topBar: {
title: "顶部标题",
welcome: "欢迎登录",
},
bottomBar: {
notes: "XXX备案,归XXX所有",
},
},
login: {
username: "用户名",
password: "密码",
},
});
i18n("home.topBar.title"); // correct
i18n("home.topBar.welcome"); // correct
i18n("home.bottomBar.notes"); // correct
// i18n('home.login.abc') // error,不存在的属性
// i18n('home.topBar') // error,没有到最后一个属性
Include
判断传入的字符串字面量类型中是否含有某个字符串
type Include<T extends string, C extends string> = T extends ""
? C extends ""
? true
: false
: T extends `${infer L}${C}${infer R}` // 可以实现 startsWith、endsWith
? true
: false;
// ---------------------------------
type a1 = Include<"hs", "J">; // true
type a2 = Include<"hs", "J">; // true
type a3 = Include<"", "">; // true 空字符串时需要特殊处理
Trim
type TrimLeft<T extends string> = T extends ` ${infer R}` ? TrimLeft<R> : T; // 去左空格
type TrimRight<T extends string> = T extends `${infer L} ` ? TrimLeft<L> : T; // 去右空格
type Trim<T extends string> = TrimRight<TrimLeft<T>>;
// ---------------------------------
type a1 = Trim<" .hs ">;
Replace
export type Replace<
T extends string,
C extends string,
RC extends string,
F extends string = ""
> = C extends ""
? T extends ""
? RC // 两方都是空,直接返回替换后的结果
: `${RC}${T}` // 如果被替换值为空,则把替换的结果换到前面
: T extends `${infer L}${C}${infer R}`
? Replace<R, C, RC, `${F}${L}${RC}`>
: F;
// ---------------------------------
type a1 = Replace<"ha ha ha", "ha", "he">;
type a2 = Replace<"hh", "hh", "hswen">;
type a3 = Replace<"a", "", "hswen">;
type a4 = Replace<"", "", "hswen">;
ComponentEmitsType
定义组件的监听事件类型
import { CamelCase } from "./10.CamelCase";
// 实现 ComponentEmitsType<Emits> 类型,将
type a1 = {
"handle-open": (flag: boolean) => true;
"preview-item": (data: { item: any; index: number }) => true;
"close-item": (data: { item: any; index: number }) => true;
};
type ComponentEmitsType<T> = {
[K in keyof T as `on${CamelCase<K & string>}`]?: T[K] extends (
...args: infer R
) => any
? (...args: R) => void
: T[K];
};
type a2 = ComponentEmitsType<a1>;
// 转化为类型
/*
{
onHandleOpen?: (flag: boolean) => void,
onPreviewItem?: (data: { item: any, index: number }) => void,
onCloseItem?: (data: { item: any, index: number }) => void,
}
*/
基于数组
LengthOfTuple
计算元组类型的长度
export type LengthOfTuple<T extends any[]> = T["length"];
// -----------------------
type A = LengthOfTuple<["B", "F", "E"]>; // 3
type B = LengthOfTuple<[]>; // 0
FirstItem
得到元组类型中的第一个元素
export type FirstItem<T extends any[]> = T[0];
// -----------------------
type A = FirstItem<[string, number, boolean]>; // string
type B = FirstItem<["B", "F", "E"]>; // 'B'
LastItem
得到元组类型中的最后一个元素
export type LastItem<T extends any[]> = T extends [...infer L, infer R]
? R
: never;
// -----------------------
type A = LastItem<[string, number, boolean]>; // boolean
type B = LastItem<["B", "F", "E"]>; // 'E'
type C = LastItem<[]>; // never
Shift
移除元组类型中的第一个类型
export type Shift<T extends any[]> = T extends [infer L, ...infer R] ? R : [];
// -----------------------
type A = Shift<[1, 2, 3]>; // [2,3]
type B = Shift<[1]>; // []
type C = Shift<[]>; // []
Push
在元组类型 T 中添加新的类型 I
export type Push<T extends any[], I> = [...T, I];
// -----------------------
type A = Push<[1, 2, 3], 4>; // [1,2,3,4]
type B = Push<[1], 2>; // [1, 2]
ReverseTuple
反转元组
export type ReverseTuple<T extends any[], F extends any[] = []> = T extends [
infer L,
...infer R
]
? ReverseTuple<R, [L, ...F]>
: F;
// -----------------------
type A = ReverseTuple<[string, number, boolean]>; // [boolean, number, string]
type B = ReverseTuple<[1, 2, 3]>; // [3,2,1]
type C = ReverseTuple<[]>; // []
Flat
拍平元组
export type Flat<T> = T extends [infer L, ...infer R]
? [...(L extends any[] ? Flat<L> : [L]), ...Flat<R>]
: T;
// -----------------------
type A = Flat<[1, 2, 3]>; // [1,2,3]
type B = Flat<[1, [2, 3], [4, [5, [6]]]]>; // [1,2,3,4,5,6]
type C = Flat<[]>; // []
type D = Flat<[1]>; // [1]
Repeat
复制类型 T 为 C 个元素的元组类型
export type Repeat<T, C, F extends any[] = []> = C extends F["length"]
? F
: Repeat<T, C, [...F, T]>;
// -----------------------
type A = Repeat<number, 3>; // [number, number, number]
type B = Repeat<string, 2>; // [string, string]
type C = Repeat<1, 1>; // [1]
type D = Repeat<0, 0>; // []
Filter
保留元组类型 T 中的 A 类型
export type Filter<T extends any[], A, F extends any[] = []> = T extends [
infer L,
...infer R
]
? Filter<R, A, [L] extends [A] ? [...F, L] : F>
: F;
// -----------------------
type A = Filter<[1, "BFE", 2, true, "dev"], number>; // [1, 2]
type B = Filter<[1, "BFE", 2, true, "dev"], string>; // ['BFE', 'dev']
type C = Filter<[1, "BFE", 2, any, "dev"], string>; // ['BFE', any, 'dev']
FindIndex
找出 E 类型在元组类型 T 中的下标
export type IsEqual<T, U, Success, Fail> = [T] extends [U]
? [U] extends [T]
? keyof T extends keyof U
? keyof U extends keyof T // 解决结构比较问题
? Success
: Fail
: Fail
: Fail
: Fail;
// IsEqual<1, any, true, false>; any判断问题
export type FindIndex<T extends any[], A, F extends any[] = []> = T extends [
infer L,
...infer R
]
? IsEqual<L, A, F["length"], FindIndex<R, A, [...F, null]>>
: never;
// -----------------------
type a1 = [any, never, 1, "2", true];
type a2 = FindIndex<a1, 1>; // 2
type a3 = FindIndex<a1, 3>; // never
TupleToEnum
元组类型转换为枚举类型
import { FindIndex } from "./25.findIndex";
type TupleToEnum<T extends any[], C = false> = {
[K in T[number]]: C extends true ? FindIndex<T, K> : K;
};
// -----------------------
// 默认情况下,枚举对象中的值就是元素中某个类型的字面量类型
type a1 = TupleToEnum<["MacOS", "Windows", "Linux"]>;
// -> { readonly MacOS: "MacOS", readonly Windows: "Windows", readonly Linux: "Linux" }
// 如果传递了第二个参数为true,则枚举对象中值的类型就是元素类型中某个元素在元组中的index索引,也就是数字字面量类型
type a2 = TupleToEnum<["MacOS", "Windows", "Linux"], true>;
// -> { readonly MacOS: 0, readonly Windows: 1, readonly Linux: 2 }
Slice
截取元组中的部分元素
export type Slice<
T extends any[],
S extends number,
E extends number = T["length"],
SA extends any[] = [],
EA extends any[] = [],
F extends any[] = []
> = T extends [infer L, ...infer R]
? SA["length"] extends S // 如果数组满足开头
? EA["length"] extends E
? [...F, L] // 如果满足结尾则结束
: Slice<R, S, E, SA, [...EA, null], [...F, L]> // 满足开头,则放入数组
: Slice<R, S, E, [...SA, null], [...EA, null], F> // 不满足开头则累加长度
: F;
// -----------------------
type A1 = Slice<[any, never, 1, "2", true, boolean], 0, 2>; // [any,never,1] 从第0个位置开始,保留到第2个位置的元素类型
type A2 = Slice<[any, never, 1, "2", true, boolean], 1, 3>; // [never,1,'2'] 从第1个位置开始,保留到第3个位置的元素类型
type A3 = Slice<[any, never, 1, "2", true, boolean], 1, 2>; // [never,1] 从第1个位置开始,保留到第2个位置的元素类型
type A4 = Slice<[any, never, 1, "2", true, boolean], 2>; // [1,'2',true,boolean] 从第2个位置开始,保留后面所有元素类型
type A5 = Slice<[any], 2>; // [] 从第2个位置开始,保留后面所有元素类型
type A6 = Slice<[], 0>; // [] 从第0个位置开始,保留后面所有元素类型
Splice
删除并且替换部分元素
export type Splice<
T extends any[],
S extends number,
E,
I extends any[] = [],
SA extends any[] = [],
EA extends any[] = [],
F extends any[] = []
> = T extends [infer L, ...infer R]
? SA["length"] extends S // 如果数组满足开头
? EA["length"] extends E
? [...F, ...I, ...T] // 如果满足结尾则,后面的都要 保留的 + 插入的 + 剩余的
: Splice<R, S, E, I, SA, [...EA, null], F> // 满足开头,计算删除个数
: Splice<R, S, E, I, [...SA, null], EA, [...F, L]> // 不满足开头,保留内容,并且累加开头长度
: F;
// -----------------------
type A1 = Splice<[string, number, boolean, null, undefined, never], 0, 2>; // [boolean,null,undefined,never] 从第0开始删除,删除2个元素
type A2 = Splice<[string, number, boolean, null, undefined, never], 1, 3>; // [string,undefined,never] 从第1开始删除,删除3个元素
type A3 = Splice<
[string, number, boolean, null, undefined, never],
1,
2,
[1, 2, 3]
>; // [string,1,2,3,null,undefined,never] 从第1开始删除,删除2个元素,替换为另外三个元素1,2,3 从第0个位置开始,保留后面所有元素类型
基于结构
OptionalKeys
获取对象类型中的可选属性的联合类型
// 拿出每一个key 来看下忽略掉后是否能赋予给原来的类型,如果可以,则说明此属性是可选属性
export type OptionalKeys<T, K = keyof T> = K extends keyof T
? Omit<T, K> extends T
? K
: never
: never;
// -------------------------
type a1 = OptionalKeys<{
foo: number | undefined;
bar?: string;
flag: boolean;
}>; // bar
type a2 = OptionalKeys<{ foo: number; bar?: string }>; // bar
type a3 = OptionalKeys<{ foo: number; flag: boolean }>; // never
type a4 = OptionalKeys<{ foo?: number; flag?: boolean }>; // foo|flag
type a5 = OptionalKeys<{}>; // never
PickOptional
保留一个对象中的可选属性类型
export type PickOptional<T> = Pick<T, OptionalKeys<T>>;
// -------------------------
type a1 = PickOptional<{
foo: number | undefined;
bar?: string;
flag: boolean;
}>; // {bar?:string|undefined}
type a2 = PickOptional<{ foo: number; bar?: string }>; // {bar?:string}
type a3 = PickOptional<{ foo: number; flag: boolean }>; // {}
type a4 = PickOptional<{ foo?: number; flag?: boolean }>; // {foo?:number,flag?:boolean}
type a5 = PickOptional<{}>; // {}
RequiredKeys
获取对象类型中的必须属性的联合类型
export type RequiredKeys<T> = Exclude<keyof T, OptionalKeys<T>>;
// ------------------------------
type a1 = RequiredKeys<{
foo: number | undefined;
bar?: string;
flag: boolean;
}>; // foo|flag
type a2 = RequiredKeys<{ foo: number; bar?: string }>; // foo
type a3 = RequiredKeys<{ foo: number; flag: boolean }>; // foo|flag
type a4 = RequiredKeys<{ foo?: number; flag?: boolean }>; // never
type a5 = RequiredKeys<{}>; // never
PickRequired
保留一个对象中的必须属性
import { RequiredKeys } from "./3.requirredKeys";
export type PickRequired<T> = Pick<T, RequiredKeys<T>>;
// ----------------------------
type a1 = PickRequired<{
foo: number | undefined;
bar?: string;
flag: boolean;
}>; // {foo:number|undefined,flag:boolean}
type a2 = PickRequired<{ foo: number; bar?: string }>; // {foo:number}
type a3 = PickRequired<{ foo: number; flag: boolean }>; // {foo:number,flag:boolean}
type a4 = PickRequired<{ foo?: number; flag?: boolean }>; // {}
type a5 = PickRequired<{}>; // {}
IsNever
判断是否为 never 类型
export type IsNever<T> = [T] extends [never] ? true : false;
// ----------------------
type A = IsNever<never>; // true
type B = IsNever<string>; // false
type C = IsNever<undefined>; // false
type D = IsNever<any>; // false
IsEmptyType
判断是否为没有属性的对象类型{}
export type IsEmptyType<T> = [keyof T] extends [never]
? unknown extends T
? false
: boolean extends T // 排除object的情况
? true
: false
: false;
type x1 = keyof {}; // never
type x2 = keyof object; // never 不能把基础类型赋予给object
type x4 = keyof unknown; // never unknown类型只能赋予给unknown
type x3 = keyof Object; // toString" | "valueOf
// ----------------------
type A = IsEmptyType<string>; // false
type B = IsEmptyType<{ a: 3 }>; // false
type C = IsEmptyType<{}>; // true
type D = IsEmptyType<any>; // false
type E = IsEmptyType<object>; // false
type F = IsEmptyType<Object>; // false
type G = IsEmptyType<unknown>; // false
IsAny
type IsAny<T> = 0 extends 1 & T ? true : false;
// 先过滤出 any 和 unknown来
// any 可以赋予给任何类型,unknown 不可以
export type IsAny<T> = unknown extends T
? [T] extends [boolean]
? true
: false
: false;
// ----------------------
type A = IsAny<string>; // false
type B = IsAny<any>; // true
type C = IsAny<unknown>; // false
type D = IsAny<never>; // false
Redux Connect
typetransform<T>=Textends(
input:Promise<inferU>)=>Promise<Action<inferS>>?(input:U)=> Action<S>:Textends(aciton: Action<inferU>)=> Action<inferS>?(action:U)=> Action<S>:never;typeConnect<T>={[KinkeyofTasT[K]extends(...args:any[])=>any?K:never]: transform<T[K]>;};typeF= Connect<Module>;// ----------------------interfaceModule{
count:number;
message:string;asyncMethod<T,U>(input:Promise<T>):Promise<Action<U>>;syncMethod<T,U>(action: Action<T>): Action<U>;}interfaceAction<T>{
payload?:T;
type:string;}// 这个要求的结果typeResult={asyncMethod<T,U>(input:T): Action<U>;syncMethod<T,U>(action:T): Action<U>;};// 实现类型Connect,要求 Connect<Module> 的结果为上面的 Result// 只要函数类型的属性;// 如果函数是异步函数,要求自动解析出来Promise中的类型;
action 的定义方式,为了测试使用
class Module {
count = 1;
message = "hello!";
asyncMethod(input: Promise<number>) {
return input.then((i) => ({
payload: i,
type: "asyncMethod",
}));
}
syncMethod(action: Action<string>) {
return {
payload: action.payload,
type: "syncMethod",
};
}
}
UnionToIntersection
逆变参数可以传父亲
将联合类型转换为交叉类型
// 先映射成函数 得到函数的联合类型// 在extends 推断参数即可exporttypeUnionToIntersection<T>=(Textendsany?(p:T)=>any:never)extends(p:inferR)=>any?R:never;typeFuncType=|((p:{ a:string})=>"人")|((p:{ b:boolean})=>"狗")|((p:{ c:number})=>"猪");typeT1={ name:string};typeT2={ age:number};typeToIntersection<T>=Textends[(x:inferU)=>any,(x:inferU)=>any]?U:never;typet3= ToIntersection<[(x:T1)=> an y,(x:T2)=>any]>;// ----------------------// type A = UnionToIntersection<{ a: string } | { b: string } | { c: string }>;// {a: string} & {b: string} & {c: string}
UnionToTuple
联合类型转换为元组类型
typeX=((p:string)=>{ a:string})&((p:number)=>{ b:string})&((p:boolean)=>{ c:number});functiona(a:string):{ a:string};functiona(a:number):{ b:string};functiona(a:boolean):{ c:string};functiona(a:string|number|boolean):{ a:string; b:string; c:string}{return{ a:"123", b:"123", c:"123"};}typeParamaterType<T>=Textends(value:inferR)=>any?R:never;typeR= ParamaterType<X>;// 先变成函数的联合类型typeFindUnionOne<T>= IsAny<T>extendstrue?any:booleanextendsT?boolean:(Textendsany?(a:(p:T)=>any)=>any:never)extends(
a:inferR)=>any?Rextends(a:inferR1)=>any?R1:void:never;// 1)先转换成交函数叉类型// 2)推断函数的参数,利用特性随机返回一个// 3) 排除boolean类型 boolean 会发生分发typeUnionToTuple<U, Last = FindUnionOne<U>>=[U]extends[never]?[]:[...UnionToTuple<Exclude<U, Last>>, Last];typea1= UnionToTuple<1|2|boolean|string>;// ----------------------typea= UnionToTuple<1|2|3>;// [1,2,3]
模板字符串以及装饰器
模板字符串类型
模板字符串类型就是将两个字符串类型值组装在一起返回。使用方式类似于 ES6 中的模板字符串。
基本使用
typename="hswen";typesayHello=`hello, ${name}`;// hello, hswen// 类型分发机制 1)typeDirection="left"|"right"|"top"|"bottom";typeAllMargin=`marigin-${Direction}`;// "marigin-left" | "marigin-right" | "marigin-top" | "marigin-bottom"// 类型分发机制 2)typeIColor="red"|"yellow"|"green";typeICount=100|200|300;typeBookSKU=`${IColor}-${ICount}`;// "red-100" | "red-200" | "red-300" | "yellow-100" | "yellow-200" | "yellow-300" | "green-100" | "green-200" | "green-300"
通过泛传入类型
typesayHello<Textendsstring|number| bigint |boolean|null|undefined>=`hello, ${T}`;// 泛型要求:string | number | bigint | boolean| null | undefiendtypeV1= sayHello<"hs">;// "Hello, hs"typeV2= sayHello<30>;// "Hello, 30"typeV3= sayHello<123n>;// "Hello, 123"typeV4= sayHello<true>;// "Hello, true"typeV5= sayHello<null>;// "Hello, null"typeV6= sayHello<undefined>;// "Hello, undefined"typev7= sayHello<string>;// `hello, ${string}`typev8= sayHello<number>;// `hello, ${number}`// 传入类型不会被解析,为所有`hello, `开头的父类型typeisChild=V1extendsv7?true:false;
映射类型中使用模板字符串
对key进行重命名
typePerson={ name:string; age:number; address:string};typeRenamePerson<T>={[KinkeyofTas`re_${K&string}`]:T[K];// K & string 保证K为string类型};let person: RenamePerson<Person>={
re_name:"hs",
re_age:30,
re_address:"回龙观",};
专用工具类型
Uppercase、Lowercase、Capitalize 、Uncapitalize
typePerson={ name:string; age:number; address:string};typePersonWithGetter<T>={[KinkeyofTas`get${Capitalize<string&K>}`]?:()=>T[K];};let person: Person ={ name:"hs", age:39, address:"回龙观"};let personGetter: PersonWithGetter<Person>={getName(){return person.name;},};
模式匹配
typeGetFristName<Sextendsstring>=Sextends`${inferF}${inferO}`?F:S;typex= GetFristName<"hs wen">;// hs
装饰器
装饰器本质就是一个函数,只能在类以及类成员上使用。TypeScript 中的装饰器可以分为类装饰器、方法装饰器、访问符装饰器、属性装饰器以及参数装饰器
类装饰器
类装饰器是直接作用在类上的装饰器,它在执行时的入参只有一个,即是这个类本身。如果装饰器函数中返回一个新的类,那么即是这个类的子类,这个子类可以用于重写父类。
const Decorator =<Textends{new(...args:any[]):{}}>(target:T)=>{(target asany).type ="动物";(target asany).getType=function(){returnthis.type;};
Object.assign(target.prototype,{eat(){console.log("eat");},drink(){console.log("drink");},});};interfaceAnimal{eat():void;drink():void;}@DecoratorclassAnimal{}const animal =newAnimal();// 原型方法
animal.eat();
animal.drink();// 静态方法console.log((Animal asany).getType());
通过返回子类的方式进行扩展
constOverrideAnimal=(target:any)=>{returnclassextends target {// 通过返回子类的方式对父类进行装饰。 最终会用子类替代targeteat(){super.eat();console.log("Override eat");}drink(){console.log("Overrided drink");}};};@OverrideAnimalclassAnimal{eat(){console.log("eat");}}const animal =newAnimal();
animal.eat();(animal asany).drink();
方法装饰器
方法装饰器的入参包括类的原型、方法名以及方法的属性描述符(PropertyDescriptor)。
functionEnum(isEnum:boolean){// 类的原型、方法名、方法属性描述符returnfunction(target:any, key:string, descriptor: PropertyDescriptor){// descriptor.enumerable 是否可枚举// descriptor.writable 是否可写// descriptor.configurable 是否能被删除// descriptor.value 原来的值
descriptor.enumerable = isEnum;// 更改属性描述符let originalEat = descriptor.value;
descriptor.value=function(...args:any[]){console.log("prev-eat");originalEat.call(this,...args);console.log("next-eat");};};}classAnimal{@Enum(true)eat(){console.log("eat");}}const animal =newAnimal();
animal.eat();
访问符装饰器
访问符装饰器本质上仍然是方法装饰器,它们使用的类型定义相同。访问符装饰器只能应用在 getter / setter 的其中一个(装饰器入参中的属性描述符都会包括 getter 与 setter 方法:)。
functionValueToUpper(
target:any,
key:string,
descriptor: PropertyDescriptor
){const original = descriptor.set;
descriptor.set=function(newValue:string){
original?.call(this, newValue.toUpperCase());};}classAnimal{private _value!:string;@ValueToUpper//将设置的值转换成大写getvalue(){returnthis._value;}setvalue(newValue:string){this._value = newValue;}}const animal =newAnimal();
animal.value ="ok";console.log(animal.value);
属性装饰器
属性装饰器在独立使用时能力非常有限,可以在类的原型上赋值来修改属性。
functionToUpper(target:any, key:string){let val ="";// "target": "ES2015" 可以进行劫持 , ESNext访问时无法劫持
Object.defineProperty(target, key,{// 给原型上添加了个属性
enumerable:true,get(){return val.toUpperCase();},set(newValue){
val = newValue;},});}classAnimal{@ToUpperpublic name:string="Animal";// 触发原型属性上的set方法}const animal =newAnimal();console.log(animal);
参数装饰器
参数装饰器包括了构造函数的参数装饰器与方法的参数装饰器,它的入参包括类的原型、参数所在的方法名与参数在函数参数中的索引值,独立使用能力依旧有限。
functionParams(target:any, key:string, index:number){// 类的原型、 参数名、参数索引console.log(target, key, index);}classAnimal{public name:string="Animal";// 触发原型属性上的set方法play(@Params val:string){console.log(val);}}
装饰器执行流程
functionEcho(val:string):any{return()=>{console.log(val);};}@Echo("类装饰器1")// 类装饰器是兜底执行@Echo("类装饰器2")// 类装饰器是兜底执行@Echo("类装饰器3")// 类装饰器是兜底执行@Echo("类装饰器4")// 类装饰器是兜底执行classFlow{constructor(@Echo("构造函数参数装饰器") str:string){}@Echo("静态方法装饰器")staticgetType(@Echo("静态方法参数装饰器") str:string){returnthis.type;}@Echo("静态属性装饰器")static type ="hello";@Echo("实例方法装饰器")handler(@Echo("实例方法参数装饰器") str:string){}@Echo("实例属性装饰器")
name!:string;@Echo("属性访问装饰器")getvalue(){return"hello";}}// [实例属性、方法(优先执行参数装饰器)、属性访问]、[静态属性、静态方法]、构造函数参数装饰器、类装饰器 (同时使用多个装饰器的执行流程“洋葱模型”)// 实例方法参数装饰器// 实例方法装饰器// 实例属性装饰器// 属性访问装饰器// 静态方法参数装饰器// 静态方法装饰器// 静态属性装饰器// 构造函数参数装饰器// 类装饰器4// 类装饰器3// 类装饰器2// 类装饰器1
- 方法装饰器,我们通常进行方法执行前后的逻辑注入。
- 属性、参数装饰器,我们通常只进行信息注册,委托别人处理。
反射元数据 Reflect Metadata
元数据:用于描述数据的数据,将信息存到 map 表中 ,最终统一操作。
反射的核心是:在程序运行时去检查以及修改程序行为,允许程序在运行时获取自身的信息。
元数据命令式定义
import "reflect-metadata";
class Animal {
static type = "哺乳类";
eat() {}
}
Reflect.defineMetadata("Class", "Animal metadata", Animal);
Reflect.defineMetadata("Class property", "type metadata", Animal, "type");
Reflect.defineMetadata("proto method", "eat metadata", Animal.prototype, "eat");
/*
=>
WeakMap => {
Animal:{
undefined:{'Class' => 'Animal metadata'},
type:{'Class property' => 'type metadata'}
},
Animal.prototype:{
eat:{'proto method' => 'eat metadata'},
}
}
*/
// 取data
console.log(Reflect.getMetadata("Class", Animal));
console.log(Reflect.getMetadata("Class property", Animal, "type"));
console.log(Reflect.getMetadata("proto method", Animal.prototype, "eat"));
// 取key
console.log(Reflect.getMetadataKeys(Animal));
console.log(Reflect.getMetadataKeys(Animal, "type"));
console.log(Reflect.getMetadataKeys(Animal.prototype, "eat"));
元数据声明式定义
@Reflect.metadata("Class", "Animal metadata")
class Animal {
@Reflect.metadata("Class property", "type metadata")
static type = "哺乳类";
@Reflect.metadata("proto method", "eat metadata")
eat() {}
}
// 在类装饰器中进行数据的消费
console.log(Reflect.getMetadata("Class", Animal));
console.log(Reflect.getMetadata("Class property", Animal, "type"));
console.log(Reflect.getMetadata("proto method", Animal.prototype, "eat"));
生成额外的metadata
开启
"emitDecoratorMetadata": true
后自动生成基于类型的元数据。
// 通过原型
console.log(Reflect.getMetadata("design:type", Animal.prototype, "eat"));
console.log(Reflect.getMetadata("design:paramtypes", Animal.prototype, "eat"));
console.log(Reflect.getMetadata("design:returntype", Animal.prototype, "eat"));
// 通过实例
console.log(Reflect.getMetadata("design:type", new Animal(), "eat"));
console.log(Reflect.getMetadata("design:paramtypes", new Animal(), "eat"));
console.log(Reflect.getMetadata("design:returntype", new Animal(), "eat"));
Required必填属性实战
import "reflect-metadata";
const REQUIRED_KEY = Symbol("required_key");
function Required(): PropertyDecorator {
return (target, prop) => {
const requiredkeys: string[] =
Reflect.getMetadata(REQUIRED_KEY, target) || [];
// 设置元数据
Reflect.defineMetadata(REQUIRED_KEY, [...requiredkeys, prop], target);
};
}
class Person {
@Required()
name!: string;
@Required()
age!: number;
}
function validate(instance: any) {
let exisitsKeys = Reflect.ownKeys(instance); // 获取已经存在的属性
let requiredKeys = Reflect.getMetadata(REQUIRED_KEY, instance) || [];
for (const key of requiredKeys) {
if (!exisitsKeys.includes(key)) {
throw new Error(key + " is required");
}
}
}
// 1)先记录哪些属性为必填属性
// 2) 在查询实例上哪个属性没有
const person = new Person();
person.name = "hh";
person.age = 30;
validate(person); // 校验属性
TypeValidation类型校验
const VALIDATION_KEY = Symbol("VALIDATION_KEY");
enum Type {
String = "string",
Number = "number",
}
function ValueType(type: Type) {
return (target: any, prop: string) => {
// 给某个属性添加元数据
Reflect.defineMetadata(VALIDATION_KEY, type, target, prop);
};
}
class Person {
@ValueType(Type.Number) // 值的类型应为number
@Required()
age!: number;
}
const instance = new Person();
// @ts-ignore
instance.age = "18";
function validate(instance: any) {
let exisitsKeys = Reflect.ownKeys(instance); // 获取已经存在的属性
let requiredKeys = Reflect.getMetadata(REQUIRED_KEY, instance) || [];
for (let key of exisitsKeys) {
let validations = Reflect.getMetadata(VALIDATION_KEY, instance, key);
if (validations) {
// 看存在的类型是否满足
if (typeof instance[key] !== validations) {
throw new Error(`${String(key)} expect ${validations}`);
}
}
}
// 校验必填属性,看实例上是否存在需要的必填属性
for (const key of requiredKeys) {
if (!exisitsKeys.includes(key)) {
throw new Error(key + " is required");
}
}
}
validate(instance);
控制反转
- 控制正转:我们去超市购物,结账时我们需要一个个自己扫描商品条形码,填写数量进行付款。整个过程由我自己控制
- 控制反转:我门去超市购物,把车推到收款区,收银员去识别条形码,最后我来付款。 控制权就被反转了。 (失去了控制权)
IoC(Inversion of Control)即控制反转,在开发中是一种设计思想。传统编程中,我们自己在对象内部创建依赖对象,即正向控制。而在 IoC 中,我们将对象的创建交给容器来控制,对象被动接受依赖,从而反转了控制关系。(解决问题:类之间的耦合度高,难以测试和重用,依赖关系的问题)
interface Monitor {}
interface Host {}
class Monitor27inch implements Monitor {}
class AppleHost implements Host {}
class Computer {
public monitor: Monitor;
public host: Host;
constructor() {
this.monitor = new Monitor27inch();
this.host = new AppleHost();
}
bootstrap() {
console.log("启动电脑");
}
}
const computer = new Computer();
computer.bootstrap();
// 组装电脑时想使用不同的零件如何实现?
根据需要手动创建并且传入零件 (手工维护依赖关系)
interface Monitor {}
interface Host {}
class Monitor27inch implements Monitor {}
class AppleHost implements Host {}
class Computer {
constructor(public monitor: Monitor, public host: Host) {}
bootstrap() {
console.log("启动电脑");
}
}
const monitor27 = new Monitor27inch();
const appleHost = new AppleHost();
const computer = new Computer(monitor27, appleHost);
computer.bootstrap();
模拟容器
interface Monitor {}
interface Host {}
class Monitor27inch implements Monitor {}
class AppleHost implements Host {}
class Computer {
constructor(public monitor: Monitor, public host: Host) {}
bootstrap() {
console.log("启动电脑");
}
}
class Container {
private instances = new Map();
bind<T>(key: string, creator: () => T) {
if (!this.instances.has(key)) {
this.instances.set(key, creator());
}
return this.instances.get(key) as T;
}
resolve<T>(key: string): T {
return this.instances.get(key) as T;
}
}
const container = new Container();
container.bind<Monitor>("Monitor", () => new Monitor27inch());
container.bind<Host>("Host", () => new AppleHost());
const computer = container.bind<Computer>(
"Computer",
() => new Computer(container.resolve("Monitor"), container.resolve("Host"))
);
computer.bootstrap();
依赖注入
DI 是 IoC 的具体体现,它是一种模式,它通过容器动态地将某个组件所需的依赖注入到组件中,而无需硬编码在组件内部。
如果代码是这个样子的,那就非常完美了~
@Provide("Monitor")
class Monitor27inch {}
@Provide("Host")
class AppleHost {}
@Provide("Computer")
class Computer {
@Inject("Monitor")
monitor!: Monitor27inch;
@Inject("Host")
host!: AppleHost;
bootstrap() {
console.log("启动电脑");
}
}
这种模式让我们可以专注于组件自身的逻辑,而不需要关心具体的依赖资源如何创建和提供。容器负责在运行时解决依赖关系,从而使代码更具可维护性和灵活性。
class Container {
private instances = new Map(); // 存储类 和 类的创造器
public properties = new Map(); // 存储属性
bind<T>(key: string, creator: () => T) {
if (!this.instances.has(key)) {
this.instances.set(key, creator());
}
return this.instances.get(key) as T;
}
resolve<T>(key: string): T {
let instance = this.instances.get(key);
for (let property of this.properties) {
// 循环所有的属性
let [key, ServiceKey] = property;
let [classKey, propKey] = key.split("-"); // 类的名字和属性名
if (instance.constructor.name !== classKey) {
// 如果不是当前类的
continue;
}
const target = this.resolve(ServiceKey); // 解析依赖
instance[propKey] = target;
}
return instance as T;
}
}
const container = new Container();
@Provide("Monitor")
class Monitor27inch {}
@Provide("Host")
class AppleHost {}
@Provide("Computer")
class Computer {
@Inject("Monitor")
monitor!: Monitor27inch;
@Inject("Host")
host!: AppleHost;
bootstrap() {
console.log("启动电脑");
}
}
// 注册到容器中
function Provide(key: string) {
return function (Target: any) {
// 保存类的名字和类的创建器
container.bind(key ?? Target.name, () => new Target());
};
}
// 注入到当前类中
function Inject(InjectKey: string) {
return function (target: any, key: string) {
// 保存注入的属性信息
container.properties.set(`${target.constructor.name}-${key}`, InjectKey);
};
}
const computer = container.resolve<Computer>("Computer");
computer.bootstrap();
依赖注入实战
import "reflect-metadata";
function methodDecorator(method: string) {
return function (path: string) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
Reflect.defineMetadata("method", method, descriptor.value);
Reflect.defineMetadata("path", path, descriptor.value);
};
};
}
export const Controller = (path?: string) => {
return function (target: any) {
Reflect.defineMetadata("path", path ?? "", target);
};
};
export const Get = methodDecorator("get");
export const Post = methodDecorator("post");
@Controller("/article")
class ArticleController {
@Get("/detail")
getDetail() {
return "get detail";
}
@Post("/add")
addArticle() {
return "post add";
}
}
function createRoutes(instance: any) {
const prototype = Reflect.getPrototypeOf(instance)!;
const rootPath = Reflect.getMetadata("path", prototype.constructor);
const methods = Reflect.ownKeys(prototype).filter(
(item) => item !== "constructor"
);
const routes = methods.map((method) => {
const requestHandler = (prototype as any)[method];
const requestPath = Reflect.getMetadata("path", requestHandler); // 获得路径
const requestMethod = Reflect.getMetadata("method", requestHandler);
return {
requestPath: `${rootPath}${requestPath}`,
requestHandler,
requestMethod,
};
});
return routes;
}
const routes = createRoutes(new ArticleController());
console.log(routes);
axios 核心实现
axios
import axios,{ AxiosRequestConfig, AxiosResponse }from"axios";const baseURL ="http://localhost:8080";// 1.定义”传递数据“和”返回数据“的接口interfacePerson{
name:string;
age:number;}let person: Person ={ name:"hswen", age:30};// 2.配置请求参数let requestConfig: AxiosRequestConfig ={
method:"get",
url: baseURL +"/get",
params: person,};// 3.发送请求,并且限制接口返回值类型axios(requestConfig).then((response: AxiosResponse)=>{return response.data;}).catch((error:any)=>{console.log(error);});
创建 axios 基本结构
axios/index.ts
classAxios{request(){}}functioncreateInstance(){// 1.创建axios实例const context =newAxios();// 2.获取request方法,并且绑定thisconst instance = Axios.prototype.request.bind(context);return instance;}// 我们真实调用的就是axios.request方法const axios =createInstance();exportdefault axios;
为了编写代码方便,我们将 Axios 类单独拿出去定义
axios/Axios.ts
classAxios{request(){}}exportdefault Axios;
创建请求及响应类型
axios/types.ts
AxiosRequestConfig
exporttypeMethods=|"get"|"GET"|"post"|"POST"|"put"|"PUT"|"delete"|"DELETE"|"options"|"OPTIONS";exportinterfaceAxiosRequestConfig{
url?:string;
method?: Methods;
params?:any;}
AxiosResponse
exportinterfaceAxiosResponse<T=any>{
data:T;
status:number;
statusText:string;
headers: Record<string,any>;
config: AxiosRequestConfig;
request?: XMLHttpRequest;}
在入口文件中导出所有类型,
export * from "./types";
编写请求方法
编写 request
exportinterfaceAxiosInstance{<T=any>(config: AxiosRequestConfig):Promise<AxiosResponse<T>>;}
用于描述 request 方法
const instance: AxiosInstance = Axios.prototype.request.bind(context);
编写请求逻辑
import { AxiosRequestConfig, AxiosResponse } from "./types";
import qs from "qs";
import parseHeader from "parse-headers";
class Axios {
request<T>(config: AxiosRequestConfig): Promise<AxiosResponse<T>> {
// 在请求前,还要实现拦截器的功能,所以先专门提供一个用于请求的方法。
// todo...
return this.dipsatchRequest(config);
}
dipsatchRequest<T>(config: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return new Promise(function (resolve, reject) {
let { method, url, params } = config;
const request = new XMLHttpRequest();
// get请求参数
if (params) {
if (typeof params === "object") {
params = qs.stringify(params);
}
url += (url!.indexOf("?") > -1 ? "&" : "?") + params;
}
request.open(method!, url!, true);
request.responseType = "json";
request.onreadystatechange = function () {
if (request.readyState === 4 && request.status !== 0) {
if (request.status >= 200 && request.status < 300) {
let response: AxiosResponse<T> = {
data: request.response ? request.response : request.responseText,
status: request.status,
statusText: request.statusText,
headers: parseHeader(request.getAllResponseHeaders()),
config,
request,
};
resolve(response);
} else {
reject("请求失败~~~");
}
}
};
request.send();
});
}
}
export default Axios;
处理 Post 请求
请求参数
let requestConfig: AxiosRequestConfig = {
method: "post",
url: baseURL + "/post",
data: person,
headers: {
"content-type": "application/json",
},
};
修改配置接口
export interface AxiosRequestConfig {
url?: string;
method?: Methods;
params?: any;
headers?: Record<string, any>;
data?: Record<string, any>;
}
修改发送逻辑
if (headers) {
for (let key in headers) {
request.setRequestHeader(key, headers[key]);
}
}
let body: string | null = null;
if (data) {
body = JSON.stringify(data);
}
request.send(body);
错误处理
网络异常错误
request.onerror = function () {
reject("net::ERR_INTERNET_DISCONNECTED");
};
可以通过
onerror
监控网络产生的异常。
超时处理
export interface AxiosRequestConfig {
// ...
timeout?: number; // 增加超时时间
}
请求参数
let requestConfig: AxiosRequestConfig = {
method: "post",
url: baseURL + "/post_timeout?timeout=3000", // 3s后返回结果
data: person,
headers: {
"content-type": "application/json",
},
timeout: 1000, // 1s后就超时
};
设置超时时间
if (timeout) {
request.timeout = timeout;
request.ontimeout = function () {
reject(`Error: timeout of ${timeout}ms exceeded`);
};
}
状态码错误
请求参数
let requestConfig: AxiosRequestConfig = {
method: "post",
url: baseURL + "/post_status?code=401", // 3s后返回结果
data: person,
headers: {
"content-type": "application/json",
},
};
设置错误信息
request.onreadystatechange = function () {
if (request.readyState === 4 && request.status !== 0) {
if (request.status >= 200 && request.status < 300) {
// ...
} else {
reject(`Error: Request faild with status code ${request.status}`);
}
}
};
拦截器
let requestConfig: AxiosRequestConfig = {
method: "post",
url: baseURL + "/post",
data: person,
headers: {
"content-type": "application/json",
name: "", // 用于记录拦截器的执行顺序
},
};
拦截器执行顺序
// 请求拦截器是倒序执行的,先放入的拦截器最后执行
let request = axios.interceptors.request.use(
(config) => {
config.headers.name += "a";
return config;
},
(err) => Promise.reject(err)
);
axios.interceptors.request.use((config) => {
config.headers.name += "b";
return config;
});
axios.interceptors.request.use((config) => {
config.headers.name += "c";
return config;
});
axios.interceptors.request.eject(request); // 放入的可以抛出来
// 响应拦截器是正序执行的,先放入的拦截器先执行
let response = axios.interceptors.response.use((response) => {
response.data.name += "a";
return response;
});
axios.interceptors.response.use((response) => {
response.data.name += "b";
return response;
});
axios.interceptors.response.use((response) => {
response.data.name += "c";
return response;
});
axios.interceptors.response.eject(response);
拦截器 promise 写法
axios.interceptors.request.use((config) => {
return new Promise((resolve) => {
setTimeout(() => {
config.headers!.name += "c";
resolve(config);
}, 3000);
});
return Promise.reject("失败了");
});
拦截器类型定义
// 强制将headers属性进行重写,变为非可选
export interface InternalAxiosRequestConfig extends AxiosRequestConfig {
headers: Record<string, any>;
}
export interface AxiosInstance {
<T = any>(config: AxiosRequestConfig): Promise<AxiosResponse<T>>;
interceptors: {
request: AxiosInterceptorManager<InternalAxiosRequestConfig>;
response: AxiosInterceptorManager<AxiosResponse>;
};
}
AxiosInterceptorManager 实现
type OnFulfilled<V> = (value: V) => V | Promise<V>;
type OnRejected = (error: any) => any;
export interface Interceptor<V> {
onFulfilled?: OnFulfilled<V>;
onRejected?: OnRejected;
}
class AxiosInterceptorManager<V> {
public interceptors: Array<Interceptor<V> | null> = [];
use(onFulfilled?: OnFulfilled<V>, onRejected?: OnRejected): number {
this.interceptors.push({
onFulfilled,
onRejected,
});
return this.interceptors.length - 1;
}
eject(id: number) {
if (this.interceptors[id]) {
this.interceptors[id] = null;
}
}
}
export default AxiosInterceptorManager;
拦截器执行原理
缺少属性 “interceptors”,但类型 “AxiosInstance” 中需要该属性
const instance: AxiosInstance = Axios.prototype.request.bind(context);
class Axios {
public interceptors = {
request: new AxiosInterceptorManager<InternalAxiosRequestConfig>(),
response: new AxiosInterceptorManager<AxiosResponse>(),
};
}
function createInstance() {
const context = new Axios();
let instance = Axios.prototype.request.bind(context);
// 3.将实例属性合并到request中
instance = Object.assign(instance, context);
return instance as AxiosInstance;
}
构建执行链
// 存放执行链路
const chain: (
| Interceptor<AxiosResponse>
| Interceptor<InternalAxiosRequestConfig>
)[] = [{ onFulfilled: this.dipsatchRequest }];
this.interceptors.request.interceptors.forEach((interceptor) => {
interceptor && chain.unshift(interceptor);
});
this.interceptors.response.interceptors.forEach((interceptor) => {
interceptor && chain.push(interceptor);
});
let promise: Promise<AxiosRequestConfig | AxiosResponse> =
Promise.resolve(config);
while (chain.length) {
const { onFulfilled, onRejected } = chain.shift()!; // 从头部删除元素
promise = promise.then(
onFulfilled as (v: AxiosRequestConfig | AxiosResponse) => any,
onRejected
);
}
// todo...
return promise as Promise<AxiosResponse<T>>;
合并配置
创建默认值对象
// 默认配置
const defaults: InternalAxiosRequestConfig = {
method: "get",
timeout: 0,
headers: {
common: {
accept: "application/json",
},
},
};
// 允许用户给defaults对象添加不同方法的默认值
let allMethods = ["delete", "get", "head", "patch", "post", "put"];
allMethods.forEach((method: string) => {
defaults.headers[method] = {};
});
设置请求 headers
if (headers) {
for (let key in headers) {
// 如果是common或是方法 就将对象合并
if (key === "common" || key === config.method) {
for (let key2 in headers[key]) {
request.setRequestHeader(key2, headers[key][key2]);
}
} else {
if (!allMethods.includes(key)) {
request.setRequestHeader(key, headers[key]);
}
}
}
}
请求与响应转换
类型声明
export interface AxiosRequestConfig {
url?: string;
method?: Methods;
params?: any;
headers?: Record<string, any>;
data?: Record<string, any>;
timeout?: number; // 增加超时时间
// 转化请求及响应类型定义
transformRequest?: (
data: Record<string, any>,
headers: Record<string, any>
) => any;
transformResponse?: (data: any) => any;
}
方法实现
// 默认配置
const defaults: InternalAxiosRequestConfig = {
method: "get",
headers: {
common: {
accept: "application/json",
},
},
// 请求前执行此方法
transformRequest: (
data: Record<string, any>,
headers: Record<string, any>
) => {
headers["content-type"] = "application/x-www-form-urlencoded";
return qs.stringify(data);
},
// 获取后执行此方法
transformResponse(data: any) {
if (typeof data == "string") data = JSON.parse(data);
return data;
},
};
request<T>(config: AxiosRequestConfig): Promise<AxiosResponse<T>> {
// 在请求前,还要实现拦截器的功能,所以先专门提供一个用于请求的方法。
config.headers = Object.assign(this.defaults.headers, config.headers);
// 合并(请求、响应)转化方法
config.transformRequest =
config.transformRequest || this.defaults.transformRequest;
config.transformResponse =
config.transformResponse || this.defaults.transformResponse;
if (config.transformRequest && config.data) {
config.data = config.transformRequest(config.data, (config.headers = {}));
}
}
request.onreadystatechange = () => {
if (request.readyState === 4 && request.status !== 0) {
if (request.status >= 200 && request.status < 300) {
// ...
// 转化响应方法
if (config.transformResponse) {
response.data = config.transformResponse(response.data);
}
resolve(response);
}
}
};
请求终止
cancelToken 的使用
const CancelToken = axios.CancelToken;
const source = CancelToken.source(); // 创建取消token
let requestConfig: AxiosRequestConfig = {
method: "post",
url: baseURL + "/post",
data: person,
cancelToken: source.token, // 请求时携带token
};
axios(requestConfig)
.then((response: AxiosResponse<Person>) => {
console.log(response.data);
return response.data;
})
.catch((error: any) => {
if (axios.isCancel(error)) {
return console.log("取消:" + error);
}
console.log(error);
});
source.cancel("用户取消请求");
取消实现原理
export class Cancel {
constructor(public message: string) {}
}
export function isCancel(value: any): value is Cancel {
return value instanceof Cancel;
}
// 取消的实现
export class CancelTokenStatic {
public resolve!: (val: Cancel) => void;
source() {
return {
// token就是一个promise
token: new Promise<Cancel>((resolve) => {
this.resolve = resolve;
}),
// 让这个promise成功,并且传入中断的原因
cancel: (reason: string) => {
this.resolve(new Cancel(reason));
},
};
}
}
声明所需类型
axios/types.ts
export interface AxiosInstance {
<T = any>(config: AxiosRequestConfig): Promise<AxiosResponse<T>>;
interceptors: {
request: AxiosInterceptorManager<InternalAxiosRequestConfig>;
response: AxiosInterceptorManager<AxiosResponse>;
};
CancelToken: CancelTokenStatic; // 取消token
isCancel: typeof isCancel; // 请求是否是被取消
}
export type CancelToken = ReturnType<CancelTokenStatic["source"]>["token"];
export interface AxiosRequestConfig {
// ...
cancelToken?: CancelToken;
}
axios/Axios.ts
if (config.cancelToken) {
config.cancelToken.then((reason: Cancel) => {
request.abort();
reject(reason);
});
}
let body: string | null = null;
if (data) {
body = JSON.stringify(data);
}
request.send(body);
//.....
装包和拆包
装包
将每个属性都被包装成了一个代理对象,用于访问和设置原始对象的属性值。
let props = {
name: "jiangwen",
age: 30,
};
type Proxy<T> = {
get(): T;
set(value: T): void;
};
type Proxify<T> = {
[P in keyof T]: Proxy<T[P]>;
};
function proxify<T>(obj: T): Proxify<T> {
let result = {} as Proxify<T>;
for (let key in obj) {
let value = obj[key];
result[key] = {
get() {
return value;
},
set: (newValue) => (value = newValue),
};
}
return result;
}
let proxpProps = proxify(props);
拆包
function unProxify<T>(proxpProps: Proxify<T>): T {
let result = {} as T;
for (let key in proxpProps) {
let value = proxpProps[key];
result[key] = value.get();
}
return result;
}
let proxy = unProxify(proxpProps);
axios 请求方法封装
import axios, { AxiosRequestConfig, AxiosInstance, AxiosResponse } from "axios";
// 用axios 进行二次封装在使用 目的就是添加一些默认的配置和拦截器
// 一般后端返回的类型都是固定的
export interface ResponseData<T = any> {
code: number;
data?: T;
msg?: string;
}
class HttpRequest {
public baseURL = "http://localhost:3000/api";
public timeout = 3000;
public request(options: AxiosRequestConfig) {
// 能自动推导就不要自己写
const instance = axios.create();
options = this.mergeOptions(options); // 合并后的选项
this.setInterceptors(instance);
return instance(options); // 可以发请求了
}
public setInterceptors(instance: AxiosInstance) {
instance.interceptors.request.use(
(config) => {
config.headers!["token"] = "xxx";
return config;
},
(err) => {
return Promise.reject(err);
}
);
instance.interceptors.response.use(
(res: AxiosResponse<ResponseData>) => {
// res.data.data
let { code } = res.data;
if (code !== 0) {
return Promise.reject(res);
}
return res;
},
(err) => {
return Promise.reject(err);
}
);
}
mergeOptions(options: AxiosRequestConfig) {
return Object.assign(
{ baseURL: this.baseURL, timeout: this.timeout },
options
);
}
public get<T = any>(url: string, data: any): Promise<ResponseData<T>> {
return this.request({
method: "get",
url,
params: data,
})
.then((res) => {
return Promise.resolve(res.data);
})
.catch((err) => {
return Promise.reject(err);
});
}
public post<T = any>(url: string, data: any): Promise<ResponseData<T>> {
return this.request({
method: "post",
url,
data,
})
.then((res) => {
return Promise.resolve(res.data);
})
.catch((err) => {
return Promise.reject(err);
});
}
}
const http = new HttpRequest();
http
.post<{ token: number; username: string }>("/login", {
username: "123",
password: "123",
})
.then((res) => {
res.data?.username;
})
.catch((err) => {
err;
});
TSConfig 详解
1.Language and Environment 语言和环境
语言和环境target指定最终生成的代码语言版本,更改 target 时会引入对应的 lib。例如指定为 es5 时,我们使用
includes
语法会发生异常,提示找不到对应的 lib。当更改为 es6 时,会自动引入对应的
lib.2015.core.d.ts
lib手动配置需要引入的类库,例如配置
DOM
,可以在页面中使用浏览器属性。同时还需手动指定 target 所配置的类库 。jsx常见的属性有
react
(编译后生成
React.createElement
方法)、
react-jsx
(编译后生成自动导入语法)、
preserve
(不进行转化,常用于 vue 中的 tsx)experimentalDecorators启用装饰器实验性语法emitDecoratorMetadata启用 metadata 生成元数据相关逻辑jsxFactory生成 react 对应的
React.createElement
或者 preact 中的 h 方法。需要在
"jsx": "react"
时使用。jsxFragmentFactory生成 react 对应的
React.Fragment
或者 preact 中的 Fragment。需要在
"jsx": "react"
时使用。文档碎片jsxImportSource配置 jsx 对应导入模块的路径,需要在
"jsx": "react-jsx"
时使用。reactNamespace生成
createElement
调用的命名空间,默认是
React
noLib禁用默认导入的所有 libuseDefineForClassFields使用 defineProperty 来定义类中的属性moduleDetection模块发现,设置为 force 时所有内容均被当做模块。其它两种模式只会将带有
import
、
export
的识别为模块。
2.Modules 模块相关
1.module
控制最终 JavaScript 产物使用的模块标准
CommonJs
、
ES6
、
ESNext
以及
NodeNext
AMD
、
UMD
、
System
等
2.rootDir
项目文件的根目录,默认推断为包含所有 ts 文件的文件夹。配合
outDir
可以看最终的输出结果。
- 如果指定后只会根据指定的路径进行编译输出。
3.moduleResolution
配置模块解析方式
node
、
Classic
、
bundler
- 在
Classic
下的模块import a from "a";
导入时会查找./a.ts
(递归往上找同名文件)。不推荐使用 - node:不支持
exports
node16
/nodenext
强制使用相对路径模块时必须写扩展名- bundler:既能使用
exports
声明类型的同时,也可以使用相对路径模块不写扩展名。
4.baseUrl
定义文件进行解析的根目录,它通常会是一个相对路径,然后配合 tsconfig.json 所在的路径来确定根目录的位置。
// baseUrl:'./'
import a from "src/a";
// 默认以tsconfig所在的路径进行解析
5.paths
类似于 alias,支持通过别名的方式进行导入。
"paths": {
"@/shared/*": ["./src/shared/*"]
}
import a from "@/shared/isString";
6.rootDirs
实现虚拟目录,告诉 TS 将这些模块视为同一层级下,但不会影响最终输出结果。可用于映射声明文件。
"rootDirs":["src/style","src/typings"]
var.module.scss
:export {
color: red;
border: 2px;
}
var.module.scss.d.ts
interface IScss {
color: string;
border: string;
}
const IScss: IScss;
export default IScss;
7.typeRoots
默认情况下,TypeScript 会在
node_modules/@types
下查找类型定义文件,可以通过设置
typeRoots
选项指定类型查找的目录。
{
"typeRoots": ["./node_modules/@types", "./typings"]
"types": [
"jquery" // 仅添加哪些声明文件
]
},
"include": ["src/**/*"] // 指定查找目录
8.allowUmdGlobalAccess
允许 umd 模块全局访问
export as namespace _;
,关闭后需要导入模块后才能访问。
@types/lodash/index.d.ts
declare const _ = _;
declare namespace _ {
export type flatten = () => void;
}
export as namespace _; // 将这个命名空间变成全局的不需要导入即可使用
export = _; // 为了用户可以导入
console.log(_); // 可以直接访问
如果文件不在@types 目录下,需要配置
include
包含此文件。
9.moduleSuffixes
模块增添后缀进行查找
[".controller", ".service"]
10.allowImportingTsExtensions
默认不允许,开启后在相对导入时就允许使用扩展名
.ts
、
.mts
、
.tsx
,注意要同时启用
--noEmit
或者
--emitDeclarationOnly
,因为这些文件导入路径还需要被构建工具进行处理后才能正常使用。
import a from "./a.mts";
11.resolvePackageJsonExports
强制 TypeScript 在从
node_modules
中的包中读取时查询
package.json
文件的
exports
字段。 在
moduleResolution
这个值为
node16
,
nodenext
, 和
bundler
时默认开启。
{
"name":"my-package",
"exports":{
".":{
"types":"./index.d.ts", // 声明文件
"import":"./index.mjs", // import导入的方式
"require": "./index.js" // requie导入的方式
}
}
}
12.resolvePackageJsonImports
强制 TypeScript 在从其祖先目录包含
package.json
的文件执行以
#
开头的查找时查询
package.json
文件的
imports
字段。
"imports": {
"#dep/*.js": "./src/utils/*.js"
}
13.customConditions
获取当 TypeScript 从 package.json 的导出或导入字段解析时要考虑的附加条件列表。
14.resolveJsonModule
启用了这一配置后,你就可以直接导入 Json 文件,并对导入内容获得完整的基于实际 Json 内容的类型推导。
15.allowArbitraryExtensions
是否以
{file basename}.d.{extension}
的形式查找该路径的声明文件。
- 文件是
app.rc
则声明文件是app.d.rc.ts
declare const style: {
color: string;
background: string;
};
export default style;
16.noResolve
不解析文件导入和三斜线指令。
模块相关module指定编译后采用的模块方式rootDir项目文件的根目录,默认推断为包含所有 ts 文件的文件夹。配合
outDir
可以看最终的输出结果。moduleResolution按照 node 方式进行模块解析。baseUrl配置项目解析的根目录,配置后可以直接通过根路径的方式导入模块。paths路径别名配置
"@/utils/*": ["src/utils/*"]
。可以使用相对路径,也可以配置
baseUrl
指定相对路径rootDirs实现虚拟目录,告诉 TS 将这些模块视为同一层级下,但不会影响最终输出结果。可用于映射声明文件。
"rootDirs":["src/a","src/b"]
typeRoots指定类型查找的目录
node_modules/@types
、
./typings
types手动指定 node_modules/@types 下需要加载的类型。allowUmdGlobalAccess允许 umd 模块全局访问
export as namespace _;
moduleSuffixes模块增添后缀进行查找
[".module", ".service"]
allowImportingTsExtensions在相对导入时就允许使用 ts 的扩展名,注意要同时启用
--noEmit
或者
--emitDeclarationOnly
,因为这些文件导入路径还需要被构建工具进行处理后才能正常使用。resolvePackageJsonExports强制 TypeScript 在从
node_modules
中的包中读取时查询
package.json
文件的
exports
字段resolvePackageJsonImports强制 TypeScript 在从其祖先目录包含
package.json
的文件执行以
#
开头的查找时查询
package.json
文件的
imports
字段。customConditions自定义条件,基本用不到resolveJsonModule解析 json 模块allowArbitraryExtensions是否以
{file basename}.d.{extension}
的形式查找该路径的声明文件。noResolve不解析文件导入和三斜线指令
3.JS 支持
javascript 相关allowJs在开启此配置后,可在
.ts
文件中去导入
.js
/
.jsx
文件。checkJs检查 js 文件,也可以通过@
ts-check
maxNodeModuleJsDepth"node_modules”检查 JavaScript 文件的最大文件夹深度。就是 node_modules 向上查找的层级
4.Emit 输出相关
1.declaration
declaration 接受一个布尔值,即是否产生声明文件 。默认不生产
2.declarationMap
引入第三方模块时,默认会查找
.d.ts
文件,配置 declarationMap 后,可以映射到原始的 ts 文件。发布 npm 包时并不会携带这些文件
3.emitDeclarationOnly
此配置会让最终构建结果只包含构建出的声明文件(
.d.ts
),而不会包含
.js
文件
4.sourceMap
创建 ts 对应的
.map
文件
5.inlineSourceMap
内嵌 sourcemap,不能与 sourceMap 属性连用
6.outFile
将所有结果打包到一个文件中(指定文件名),仅支持
amd
和
system
模块
7.outDir
将所有生成的文件发射到此目录中
8.removeComments
移除 ts 文件内的注释
9.noEmit
在编译过程中不生成文件,但是编译过程中会进行类型检测。
10.importHelpers
基于 target 进行语法降级,往往需要一些辅助函数,将新语法转换为旧语法的实现。启用 importHelpers 配置,这些辅助函数就将从 tslib 中导出而不是在源码中定义。
需要安装
tslib
,并且开启
moduleResolution
选项。
11.importsNotUsedAsValues
是否保留导入后未使用的导入值,默认则删除。此属性被
verbatimModuleSyntax
替代
import Car from "./car"; // 导入的是类型,默认会被移除。应该使用import type
function buyCar(car: Car) {
return car;
}
12.downlevelIteration
是否开启对 iterator 降级处理,默认在低版本中直接转化成索引遍历
let arr = [1, 2, 3];
for (let key of arr) {
console.log(arr);
}
13.sourceRoot
在 debugger 时,用于定义我们的源文件的根目录。
14.mapRoot
在 debugger 时,用于定义我们的
source map
文件的根目录。
15.inlineSources
增加 sourcesContent,压缩后依然可以找到对应的源代码
16.emitBOM
生成 BOM 头
17.newLine
换行方式
crlf
(Carriage Return Line Feed)widows 系统的换行符。
lf
(Line Feed)Linux 系统的换行方式
18.stripInternal
是否禁止 JSDoc 注释中带有@internal 的代码发出类型声明
/**
* @internal
*/
const a = "abc";
export default a;
19.noEmitHelpers
在开启时源码中仍然会使用这些辅助函数,但是不存在从 tslib 中导入的过程,同时需要将
importHelpers
关闭。
export function merge(o1: object, o2: object) {
return { ...o1, ...o2 };
}
20.noEmitOnError
构建过程中有错误产生会阻止写入
21.preserveConstEnums
让常量枚举也转化成对象输出
22.declarationDir
指定声明文件输出的目录
23.preserveValueImports
保留所有值导入,不进行移除。(未用到也进行保留,已经废弃) ,同
importsNotUsedAsValues
输出相关declaration是否产生声明文件declarationMap为声明文件也生成 source map,通过
.d.ts
映射到
.ts
文件emitDeclarationOnly仅生成
.d.ts
文件,不生成
.js
文件sourceMap创建 js 对应的
.map
文件outFile将所有结果打包到一个文件中,仅支持
amd
和
system
模块outDir将所有生成的文件发射到此目录中removeComments移除 ts 文件内的注释noEmit在编译过程中不生成文件,但是编译过程中会进行类型检测。importHelpers从
tslib
中引入辅助函数解析高版本语法
{...obj}
importsNotUsedAsValues是否保留导入后未使用的导入值downlevelIteration是否开启对 iterator 降级处理,默认在低版本中直接转化成索引遍历sourceRoot在 debugger 时,用于定义我们的源文件的根目录。mapRoot在 debugger 时,用于定义我们的
source map
文件的根目录。inlineSourceMap内嵌 sourcemap,不能与 sourceMap 属性连用inlineSources内链 sourcesContent 属性,压缩后依然可以找到对应的源代码emitBOM生成 BOM 头newLine换行方式
crlf
(Carriage Return Line Feed)widows 系统的换行符。
lf
(Line Feed)Linux 系统的换行方式stripInternal是否禁止 JSDoc 注释中带有@internal 的代码发出声明noEmitHelpers不从 tslib 中导入辅助函数noEmitOnError构建过程中有错误产生会阻止写入preserveConstEnums让常量枚举也转化成对象输出declarationDir指定声明文件输出的目录preserveValueImports保留所有值导入,不进行移除。(未用到也进行保留,已经废弃)
5.Interop Constraints 互操作约束
1.isolatedModules
隔离模块,重导出一个类型需要使用
export type
。
2.verbatimModuleSyntax
取代 isolatedModules、preserveValueImports、importsNotUsedAsValues。
import type
就删除,
import
就留下。
互操作约束isolatedModules隔离模块,文件中需要包含
import
、
export
,导入类型需要使用
import type
进行导入verbatimModuleSyntax取代 isolatedModules、preserveValueImports、importsNotUsedAsValuesallowSyntheticDefaultImports解决 ES Module 和 CommonJS 之间的兼容性问题。模拟默认导出。esModuleInterop解决 ES Module 和 CommonJS 之间的兼容性问题。可以支持
import React from 'react'
。会自动开启
allowSyntheticDefaultImports
preserveSymlinks不把符号链接解析为真实路径forceConsistentCasingInFileNames强制文件名使用时大小写一致
3.allowSyntheticDefaultImports
解决 ES Module 和 CommonJS 之间的兼容性问题。(输出成
module:commonjs
)
function sum(a: number, b: number) {
return a + b;
}
export = sum;
import sum from "./sum"; // es6方式导入
兼容模块间转换,模拟
commonjs
默认导出。
4.esModuleInterop
默认开启,解决 ES Module 和 CommonJS 之间的兼容性(
.default
)问题。可以支持
import React from 'react'
。会自动开启
allowSyntheticDefaultImports
5.preserveSymlinks
是否禁用将符号链接解析为其真实路径 (开启后等价于
webpack.resolve.symlinks
为 false )。webpack 中大多数情况下采用
symlinks:true
(Webpack 会按照符号链接的实际位置来解析模块,这是通常的行为。)
6.forceConsistentCasingInFileNames
强制文件名使用时大小写一致
6.Type Checking 类型检测
1.strict
设置为 true 会启用全部类型检测选项,同时也可以指定单独关闭某个具体的类型检测的选项
2.noImplicitAny
为具有隐含“any”类型的表达式和声明启用错误报.
3.strictNullChecks
开启此选项让 typescript 执行严格的 null 检查
4.strictFunctionTypes
开启后支持函数参数的双向协变
5.strictBindCallApply
请检查“bind”、“call”和“apply”方法的参数是否与原始函数匹配。
6.strictPropertyInitialization
检查构造函数中已声明但未设置的类属性。
7.noImplicitThis
当“this”的类型为“any”时,报错。
8.useUnknownInCatchVariables
将 catch 变量默认为“unknown”,而不是“any”。
9.alwaysStrict
确保输出文件始终带有 “use strict”
10.noUnusedLocals
当 ts 发现未使用的局部变量时, 会给出一个编译时错误
11.noUnusedParameters
当 ts 发现参数未使用时, 会给出一个编译时错误
12.exactOptionalPropertyTypes
默认值为 false,将可选属性类型解释为写入,而不是添加“未定义”。在初始化时可以留空为
undefined
, 但是不能被手动设置为
undefined
13.noImplicitReturns
默认值为 false,开启这个选项,所有分支都要有 return。
14.noFallthroughCasesInSwitch
默认值为 false,开启这个选项,每个 switch 中的 case 都要有 break;
15.noUncheckedIndexedAccess
默认值为 false,开启这个选项,给索引签名语法声明的属性补上一个
undefined
类型
16.noImplicitOverride
默认值为 false,开启这个选项,保证子类重写基类的方法时, 必须在方法前加上
override
关键词
17.noPropertyAccessFromIndexSignature
默认值为 false,开启这个选项,禁止通过访问常规属性的方法来访问索引签名声明的属性。
18.allowUnusedLabels
默认值为 false,开启这个选项后,允许没有使用的 label
19.allowUnreachableCode
默认值为 false,开启这个选项后,则允许出现无法触达的代码
类型检查strict启用所有严格类型检测选项noImplicitAny关闭后,没有指定参数类型时,默认推导为 anystrictNullChecks关闭后,null 和 undefiend 将会成为任何类型的子类型strictFunctionTypes关闭后,参数变为双向协变strictBindCallApply关闭后,不检测 call、bind、apply 传递的参数。strictPropertyInitialization关闭后,函数声明属性无需初始化操作。noImplicitThis关闭后,this 默认推导为 anyuseUnknownInCatchVariables关闭后,catch 中的 error 类型会变为 any。alwaysStrict关闭后,不使用严格模式noUnusedLocals关闭后,允许声明未使用的变量noUnusedParameters关闭后,允许声明未使用的参数exactOptionalPropertyTypes开启后,进行严格可选属性检测,不能赋予 undefinednoImplicitReturns开启后,要求所有路径都需要有返回值。noFallthroughCasesInSwitch开启后,switch、case 中不能存在连续执行的情况。noUncheckedIndexedAccess任意接口中访问不存在的属性会在尾部添加
undefiend
类型noImplicitOverride增添 override 关键字,才可以覆盖父类的方法noPropertyAccessFromIndexSignature不允许访问任意接口中不存在的属性allowUnusedLabels是否允许未使用的 label 标签allowUnreachableCode是否允许无法执行到的代码
7.Completeness 完整性
完整性skipLibCheck跳过类库检测,不检测内置声明文件及第三方声明文件。skipDefaultLibCheck跳过 TS 库中内置类库检测。
8.Projects 项目
1.incremental
incremental 配置将启用增量构建,在每次编译时首先 diff 出发生变更的文件,仅对这些文件进行构建,然后将新的编译信息通过
.tsbuildinfo
存储起来。
2.tsBuildInfoFile
控制这些编译信息文件的输出位置。
3.composite
在 Project References 的被引用子项目
tsconfig.json
中必须为启用状态。并且在子项目中必须启用 declaration ,必须通过 files 或 includes 声明子项目内需要包含的文件等。
项目相关incremental启用增量构建, 当使用–watch 的时候可以配合开启composite被 references 引用的
tsconfig.json
必须标识为 truetsBuildInfoFile增量构建文件的存储路径disableSourceOfProjectReferenceRedirect在引用复合项目时首选源文件而不是声明文件。disableSolutionSearching编辑时,选择不检查多项目引用的项目。disableReferencedProjectLoad禁用引用项目加载
9.其他
1.files、include 与 exclude
- 使用 files 我们可以描述本次包含的所有文件,每个值都需要是完整的文件路径,适合在小型项目时使用。
{
"include": ["src/**/*", "utils/*.ts"],
"exclude": ["src/file-excluded", "/**/*.test.ts", "/**/*.e2e.ts"]
}
exclude 只能剔除已经被 include 包含的文件。
2.extends
tsconfig.base.json
{
"compilerOptions": {
"target": "ES6",
"module": "CommonJS",
"strict": true,
"esModuleInterop": true
},
"include": ["src/**/*"]
}
tsconfig.json
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist"
}
}
3.references
可以将整个工程拆分成多个部分,我们可以定义这些部分的引用关系,为它们使用独立的 tsconfig 配置。
- root- index.ts
import user from "../user";console.log(user());
- tsconfig.json{ "extends": "../tsconfig.json", "compilerOptions": { "target": "ES2015", "baseUrl": ".", "outDir": "../dist/root" }, "include": ["./**/*.ts"], "references": [ { "path": "../user" } ]}
- user- index.ts
export default function () { return "get user";}
- tsconfig.json{ "extends": "../tsconfig.json", "compilerOptions": { "composite": true, "target": "ES5", "module": "NodeNext", "baseUrl": ".", "outDir": "../dist/user" }, "include": ["./**/*.ts"]}
- tsconfig.json
{ "compilerOptions": { "declaration": true, "module": "NodeNext", "moduleResolution": "NodeNext" }}
tsc --build <要打包的文件夹>
4.watchOptions
监听选项,一般不进行配置
"watchOptions": {
// 如何监听文件 使用操作系统的原生事件来进行监听
"watchFile": "useFsEvents",
// 如何监听目录
"watchDirectory": "useFsEvents",
// 对变更不频繁的文件,检查频率降低
"fallbackPolling": "dynamicPriority",
"synchronousWatchDirectory": true,
"excludeDirectories": ["**/node_modules", "_build"],
"excludeFiles": ["build/fileWhichChangesOften.ts"] // 减少更新范围
}
版权归原作者 若梦plus 所有, 如有侵权,请联系我们删除。