学习林不渡的TS入门小册总结-TS入门一卡通

185 阅读18分钟

TS的安装与配置

 # ts的安装
 pnpm install -g typescript
 ​
 # ts-node运行环境
 pnpm install -g ts-node 
 ​
 # esno 类似 ts-node的运行环境
 pnpm install -g esno

VSCode设置TypeScript类型提示

  • 通过 Ctrl(Command) + Shift + P 打开命令面板,找到「打开工作区设置」这一项。
  • 在打开的设置中输入 typescript,筛选出所有 TypeScript 有关的配置,点击左侧的"TypeScript",这里才是官方内置的配置。
  • 在设置输入框中补全搜索词,使用“typescript inlay hints”
  • 开启这里的所有相关类型提示配置
 "typescript.inlayHints.enumMemberValues.enabled": true
 "typescript.inlayHints.functionLikeReturnTypes.enabled": true
 "typescript.inlayHints.parameterNames.enabled": "all"
 "typescript.inlayHints.parameterNames.suppressWhenArgumentMatchesName": true
 "typescript.inlayHints.parameterTypes.enabled": true
 "typescript.inlayHints.propertyDeclarationTypes.enabled": true
 "typescript.inlayHints.variableTypes.enabled": true
 "typescript.inlayHints.variableTypes.suppressWhenTypeMatchesName": true

TS的数据类型

TypeScript中的数据类型可以分为原始类型和对象类型两类。

 const userNick:string = 'linbudu'; // 字符串
 const userAge:number = 18; // 数字
 const userMarried:boolean = false; // 布尔值
 const userNull:null = null; // null
 const userUndefined:undefined = undefined; // undefined
 ​
 // 对象
 const userObject:object = {}; 
 const userObject:{} = {};   
 ​
  // 数组
 const userList:string[] = [];
 const userList:Array<string> = [];
 ​
 const user: User = {
   userName: 'test',
   userAge: 20,
   userMarried: false,
 };
 const userList: User[] = [...]  // ...表示省略的User对象类型
 ​
 // 枚举类型  对于数字类型的值,枚举类型能够自动累加值 枚举中可以同时支持数字、字符串、函数计算等成员
 enum UserLevelCode {
   Visitor = 10001,
   NonVIPUser = 10002,
   VIPUser = 10003,
   Admin = 10010,
   // ... 
 }
 ​
 // Set类型 类似Array<类型>
 ​
 ​
 // Map类型 类似Array<类型>
 ​
 ​
 ​
 // ? 表示可由可无
 const name?:string = "Hello";   

TS的函数类型与重载

TS中的函数表达式和函数声明式

 const handler = function (args) {}; // 函数表达式
 const handler = (args) => {}; //  箭头函数表达式
 ​
 function handler(args) { }; // 函数声明

声明与函数表达式的一个重要区别在于,函数声明是允许调用写在声明之前的从这个角度看,函数表达式就像是声明了一个变量,在执行到这里时才完成了函数的创建,然后在下面的代码里才能够调用。而函数声明则是向当前作用域做了广播:这里有一个 handler 函数,欢迎你随时随地调用我。(和C语言类似)

TS中的函数声明和函数表达式带类型

 function sum(a: number, b: number): number {
   return a + b;
 }
 ​
 const sum = function(a: number, b: number): number {
   return a + b;
 };
 ​
 type Sum = (a: number, b: number) => number;
 ​
 const sum: Sum = function(a, b) {
   return a + b;
 };

在 JavaScript 的函数中,如果没有显式的 return 语句,那么这个函数的执行结果实际会是 undefined,但在 TypeScript 中,需要将这个函数的返回值类型标注为 void 而不是 undefined

 function handler1(): void {}; // √
 function handler2(): undefined {}; // X

在 TypeScript 中,undefined 也被视为一个有意义的类型,希望将返回值类型标注为 undefined,就需要有显式的 return 语句

 function handler2(): undefined {
   return;
 };

5.1 版本中,TS 对这个不符直觉的问题进行了修正,即允许了 undefined 作为无显式 return 语句函数的返回值类型,但考虑到发布时间较晚,因此还是有必要了解这个问题的。

javascript实现函数重载

 function sum(x, y) {
   if (typeof x === 'number' && typeof y === 'number') {
     return x + y;
   } else if (Array.isArray(x) && typeof y === 'number') {
     return x.map((num) => num + y);
   } else if (typeof x === 'number' && Array.isArray(y)) {
     return y.map((num) => num + x);
   } else if (Array.isArray(x) && Array.isArray(y)) {
     if (x.length !== y.length) {
       throw new Error('Arrays must have the same length');
     }
     return x.map((num, index) => num + y[index]);
   } else {
     throw new Error('Invalid arguments');
   }
 }
 ​
 console.log(sum(2, 3)); // 5
 console.log(sum([1, 2, 3], 4)); // [5, 6, 7]
 console.log(sum(5, [1, 2, 3])); // [6, 7, 8]
 console.log(sum([1, 2, 3], [4, 5, 6])); // [5, 7, 9]
 console.log(sum('a', 'b')); // Error: Invalid arguments
 console.log(sum([1, 2, 3], [4, 5])); // Error: Arrays must have the same length
 ​
 // 标注类型
 function sum(numberOrArray, numberOrArray) { }

typescript支持了类型层面的函数重载

 function sum(base: number, incre: number): number;
 function sum(baseArray: number[], incre: number): number[];
 function sum(incre: number, baseArray: number[]): number[];
 function sum(baseArray: number[], increArray: number[]): number[];
 function sum(x: number | number[], y: number | number[]): number | number[] { }

TypeScript 中的函数重载还是属于伪重载,它只能在类型层面帮你实现重载的效果,而实际的逻辑运行,由于 JavaScript 不支持,它也就束手无策了。真正的函数重载应该是直接定义多个同名的函数,这些函数的内部逻辑是仅服务一套参数组合的。(参考Java的函数重载)

TS的面向对象编程

面向对象编程:

 class Circle {
   constructor(radius) {
     // 要描述圆形,最重要的一个属性就是半径
     this.radius = radius;
   }
   
   getArea() {
     return Math.PI * this.radius ** 2;
   }
   
   getCircumference() {
     return 2 * Math.PI * this.radius;
   }
 }
 ​
 const circle = new Circle(5);
 ​
 console.log(`面积为:${circle.getArea()}, 周长为:${circle.getCircumference()}.`);
 ​
 ​
 ​
 ​

面向过程编程:

 function getArea(radius) {
   return Math.PI * radius ** 2;
 }
 ​
 function getCircumference(radius) {
   return 2 * Math.PI * radius;
 }
 ​
 const radius = 5;
 console.log(`面积为:${getArea(radius)}, 周长为:${getCircumference(radius)}.`);
 ​

ES6中的面向对象编程(Class)使用

 class Person {
   name;
   age;
 ​
   constructor(personName, personAge) {
     this.name = personName;
     this.age = personAge;
   }
   
   getDesc(): string {
     return `${this.name} at ${this.age} years old`;
   }
 }
 ​
 // 实例化
 const person1 = new Person("xxx", 18);
 const person2 = new Person("Charles", 20);
 ​
 person1.name; // xxx
 person1.getDesc(); // xxx at 18 years old

ES6面向过程的写法:

 const person1 = {
   name: 'xxx',
   age: 18
 }
 ​
 const getPersonDesc = (person) => {
   return `${person.name} at ${person.age} years old`;
 }
 ​

通过继承 Person ,额外添加属性和方法来实现一个新的对象:

 class School {}
 ​
 class Student extends Person {
   grade: number;
   school: School;
 }
 ​
 class Job {}
 ​
 class Worker extends Person {
   salary: number;
   job: Job;
 }
 ​

需要 Class 的重要原因:封装性 继承性

typescript中的面向对象:

 class Person {
   private name: string;     // 可以使用public
   private age: number;      // 可以使用public
 ​
   constructor(personName: string, personAge: number) {
     this.name = personName;
     this.age = personAge;
   }
 ​
   public getDesc(): string {
     return `${this.name} at ${this.age} years old`;
   }
 ​
   public getName(): string {
     return this.name;
   }
 ​
   public getUpperCaseName(): string {
     return this.name.toLocaleUpperCase();
   }
 }
 ​
 const person = new Person('xxx', 18);
 ​
 console.log(person.name); // 属性“name”为私有属性,只能在类“Person”中访问。
 console.log(person.getName()); // xxx
 console.log(person.getUpperCaseName()); // LINBUDU
 ​
 ​
 ​
 // 面向对象的两种写法
 interface FoodInterface {
     type: string;
 }
 ​
 class FoodClass implements FoodInterface {
     constructor(public type:string) {}
 }
 ​
 // 等价于
 class FoodClass implements FoodInterface {
     type:string;
     constructor(arg:string) {
         this.type = arg;
     }
 }
 ​
 // 接口可以继承类
 1.接口可以继承类,当接口继承类之后,会继承成员(类型),但是不包括实现;
 2.接口还会继承privateprotected修饰的成员,但是这个接口只可以被这个类或它的子类实现。

Class 中的方法也支持重载

 class Person {
   feedPet(catFood: CatFood): void;
   feedPet(dogFood: DogFood): void;
   feedPet(rabbitFood: RabbitFood): void;
   feedPet(food: CatFood | DogFood | RabbitFood): void {}
 }

Class作为工具方法的命名空间,将一批功能类似的方法收拢到一个 Class 内部

 export class DateUtils {
   static isSameDate(){ }
   static diffDate(){ }
 }
 ​
 export class NumberUtils { }
 export class UserListUtils { }
 // ...
 ​
 ​
 // 这里的 static 称为“静态成员”,可以不实例化就直接访问这个成员
 import { DateUtils } from './utils';
 ​
 DateUtils.isSameDate();
 ​
 // 如图片地址、配置信息这样的常量,也可以使用 Class + 静态成员来定义
 ​

TS的anyunknown类型与类型断言

any 类型 = string + number + boolean + 任意对象类型 + 拥有任意参数类型与任意返回值类型的函数类型 + ...,当不知道对一个变量提供何种类型时,就可以使用 any 类型来作为临时性的过渡方案。

any 类型 = 万能类型 + 放弃类型检查

考虑到 any 类型的危险性,TypeScript 中还提供了一个功能类似的家伙:unknown 类型,用于表示万能类型的同时,保留类型检查。

函数内使用unknown

 function myFunc(param: unknown) {
   param.forEach((element) => {}); // X “param”的类型为“未知”。
 }
 ​

将 unknown 类型的变量断言到数组类型

 function myFunc(param: unknown) {
   (param as number[]).forEach((element) => {
     element = element + 1;
   });
 }
 ​
 function myFunc(param: unknown) {
   (param as unknown[]).forEach((element) => {
     element = (element as number) + 1;
   });
 }
 ​

小结:any 类型和 unknown 类型都能提供万能类型的作用,但不同之处在于,使用 any 类型后就丧失了类型检查的保护,可以对变量进行任意操作。而使用 unknown 类型时,虽然每进行一次操作都需要进行类型断言,断言到当前我们预期的类型,但这却能实现类型信息反向补全的功能,为最终的具体类型埋下伏笔。虽然 any 类型的使用过程中也可以通过类型断言保障,但毕竟缺少了类型告警,很容易被忽略。

常见的场景是将一个拥有具体类型的变量断言到 any / unknown 类型:

 const str: string = "linbudu";
 ​
 (str as any).handler().result.prop; // ...
 ​

某些时候 TypeScript 的类型分析会显得不那么符合直觉

 interface IUser {
   name: string;
   job?: IJob;
 }
 ​
 interface IJob {
   title: string;
 }
 ​
 const user: IUser = {
   name: 'foo',
   job: {
     title: 'bar',
   },
 };
 ​
 const { name, job = {} } = user;
 ​
 const { title } = job; // 类型“{}”上不存在属性“title”。
 ​
 // 解决方案,可以在第一次解构赋值时将这个空对象断言到预期的类型:
 const { name, job = {} as IJob } = user;
 ​
 const { title } = job;

TS的类型体操

在 JavaScript 中需要定义变量和函数的两个关键:引用和复用。

函数也是类似,定义一个函数就是为了抽象一段通用的数据转换逻辑,然后再提供给其它地方的逻辑消费。

使用类型别名存储一个函数类型:

 type Handler = () => void;
 ​
 const handler1: Handler = () => {};
 const handler2: Handler = () => {};
 ​

使用类型别名来替换接口,实现对对象类型的复用

 type User =  {
   userName: string;
   userAge: number;
   userMarried: boolean;
   userJob?: string;
 }
 ​
 const user: User = { /* ... */ }
 ​

类型别名也可以像函数一样接受参数并返回计算结果

类型的联合类型,需要使用类型别名来存放

 type PossibleTypes = string | number | boolean;
 ​
 // 使用联合类型
 let foo: PossibleTypes = "LinYu";
 foo = 599;
 foo = true;

联合类型对其中的类型成员并没有限制,你可以混合原始类型,字面量类型,函数类型,对象类型等,最场景的是字面量联合类型,它表示一组精确的字面量类型:

 type Status = 'success' | 'failure';
 type Code = 200 | 404 | 502;

字面量类型和联合类型简直就是天生一对

 const fixedStr: 'linbudu' = 'linbudu'; // 值只能是 'linbudu'
 const fixedNum: 599 = 599; // 值只能是 599
 ​
 const literalString: 'linbudu' = 'linbudu';
 const literalNumber: 599 = 599;
 const literalBoolean: true = true;
 const literalObject: { name: 'linbudu' } = { name: 'linbudu' };
 const literalArray: [1, 2, 3] = [1, 2, 3];
 ​

为什么需要字面量类型?

因为字面量联合类型相比它们对应的原始类型,能够提供更精确的类型信息与类型提示。

理想情况下,如请求状态与用户类型这样值被固定在一个小范围内的属性,都应该使用字面量联合类型进行标注。

除了基于字面量类型的小范围精确标注,也可以使用由接口组成的联合类型:

 interface VisitorUser {}
 interface CommonUser {}
 interface VIPUser {}
 interface AdminUser {}
 ​
 type User = VisitorUser | CommonUser | VIPUser | AdminUser;
 ​
 const user: User = {
   // ...任意实现一个组成的对象类型
 }

类型别名表示一个交叉类型

 interface UserBasicInfo {}
 interface UserJobInfo {}
 interface UserFamilyInfo {}
 ​
 type UserInfo = UserBasicInfo & UserJobInfo & UserFamilyInfo;       // 交叉类型使用的是&
 // 上述交叉类型等效于
 // 伪代码
 type UserInfo = {
   ...UserBasicInfo,
   ...UserJobInfo,
   ...UserFamilyInfo
 }
 ​
 // 交叉两个原始类型
 type Test = string & number; // never 类型
 ​
 ​

联合类型与交叉类型也可以一起使用:

 // 伪代码 先交叉再联合
 type Reward = (FE & React) | (OutstandingAuthors & PostLastMonth);
 ​
 // 先联合再交叉
 type UnionIntersection = (1 | 2 | 3) & (1 | 2); // 1 | 2
 ​

TS的泛型:类型世界的参数

类型别名能够充当一个变量,存放一组存在关联的类型:

 type Status = 'success' | 'failure' | 'pending';

类型别名还能够充当函数的作用(泛型):

 type Status<T> = 'success' | 'failure' | 'pending' | T;
 ​
 type CompleteStatus = Status<'offline'>;

这里的 CompleteStatus ,其实等价于:

 type CompleteStatus = 'success' | 'failure' | 'pending' | 'offline';

泛型类比为函数来理解:

 // 伪代码
 function Status(T){
   return ['success', 'failure', 'pending', T]
 }
 ​
 const CompleteStatus = Status('offline');
 ​

多泛型声明:

 function factory<T1, T2, T3>(input: T1, arg1: T2, arg2: T3): T1 {
   // ...
 }
 ​

TS的类型声明

样例代码:

 // CSS modules
 type CSSModuleClasses = { readonly [key: string]: string }
 ​
 declare module '*.module.css' {
   const classes: CSSModuleClasses
   export default classes
 }
 declare module '*.module.scss' {
   const classes: CSSModuleClasses
   export default classes
 }
 declare module '*.module.sass' {
   const classes: CSSModuleClasses
   export default classes
 }
 // ...
 ​

概括地说,类型声明文件就是一种不包括任何实际逻辑,仅仅包含类型信息,并且无需导入操作,就能够被 TypeScript 自动加载的文件。也就是说,如果定义了类型声明文件,即使你都不知道这个文件放在哪里了,其中的类型信息也能够被加载,然后成为你开发时的类型提示来源。

除了模块声明以外,还有一种常见的声明是对变量的声明。

 declare var window: Window & typeof globalThis;
 ​
 interface Window {
   // ...
 }
 ​
 // declare var 这个语法称为变量类型声明,是将这个类型声明提供到全局
 // var 声明变量意味着这个变量在全局作用域可用

TS的内置工具类型

Partial接收一个对象类型,并将这个对象类型的所有属性都标记为可选

 type User = {
   name: string;
   age: number;
   email: string;
 };
 ​
 type PartialUser = Partial<User>;
 ​
 const user: User = {
   name: 'John Doe',
   age: 30,
   email: 'john.doe@example.com'
 };
 ​
 // 可以不实现全部的属性了!
 const partialUser: PartialUser = {
   name: 'John Doe',
   age: 30
 };
 ​

Required将属性标记为必选

 type User = {
   name?: string;
   age?: number;
   email?: string;
 };
 ​
 type RequiredUser = Required<User>;
 ​
 const user: User = {
   name: 'John Doe'
 };
 ​
 // 现在你必须全部实现这些属性了
 const requiredUser: RequiredUser = {
   name: 'John Doe',
   age: 30,
   email: 'john.doe@example.com'
 };

readonly将属性标记为只读,只读通常是一个不可逆的行为

 type User = {
   name: string;
   age: number;
   email: string;
 };
 ​
 type ReadonlyUser = Readonly<User>;
 ​
 const user: User = {
   name: 'John Doe',
   age: 30,
   email: 'john.doe@example.com'
 };
 ​
 const readonlyUser: ReadonlyUser = {
   name: 'John Doe',
   age: 30,
   email: 'john.doe@example.com'
 };
 ​
 // 修改 user 对象的属性
 user.name = 'Jane Doe';
 user.age = 25;
 user.email = 'jane.doe@example.com';
 ​
 // 修改 readonlyUser 对象的属性
 // readonlyUser.name = 'Jane Doe';  // 报错
 // readonlyUser.age = 25;  // 报错
 // readonlyUser.email = 'jane.doe@example.com';  // 报错

Record用于声明一个内部属性键与键值类型一致的对象类型

 type UserProps = 'name' | 'job' | 'email';
 ​
 // 等价于你一个个实现这些属性了
 type User = Record<UserProps, string>;
 ​
 const user: User = {
   name: 'John Doe',
   job: 'fe-developer',
   email: 'john.doe@example.com'
 };
 ​
 // 使用 Record 类型来声明属性名还未确定的接口类型
 type User = Record<string, string>;
 ​
 const user: User = {
   name: 'John Doe',
   job: 'fe-developer',
   email: 'john.doe@example.com',
   bio: 'Make more interesting things!',
   type: 'vip',
   // ...
 };

Pick 类型接收一个对象类型,以及一个字面量类型组成的联合类型,这个联合类型只能是由对象类型的属性名组成的。它会对这个对象类型进行裁剪,只保留传入的属性名组成的部分

 type User = {
   name: string;
   age: number;
   email: string;
   phone: string;
 };
 ​
 // 只提取其中的 name 与 age 信息
 type UserBasicInfo = Pick<User, 'name' | 'age'>;
 ​
 const user: User = {
   name: 'John Doe',
   age: 30,
   email: 'john.doe@example.com',
   phone: '1234567890'
 };
 ​
 const userBasicInfo: UserBasicInfo = {
   name: 'John Doe',
   age: 30
 };

Omit类型就是 Pick 类型的另一面,它的入参和 Pick 类型一致,但效果却是相反的——它会移除传入的属性名的部分,只保留剩下的部分作为新的对象类型

 type User = {
   name: string;
   age: number;
   email: string;
   phone: string;
 };
 ​
 // 只移除 phone 属性
 type UserWithoutPhone = Omit<User, 'phone'>;
 ​
 const user: User = {
   name: 'John Doe',
   age: 30,
   email: 'john.doe@example.com',
   phone: '1234567890'
 };
 ​
 const userWithoutPhone: UserWithoutPhone = {
   name: 'John Doe',
   age: 30,
   email: 'john.doe@example.com'
 };

Pick 与 Omit 类型是类型编程中相当重要的一个部分.

举例:

声明一个代表全局所有状态的大型接口类型:

 type User = {
   name: string;
   age: number;
   email: string;
   phone: string;
   address: string;
   gender: string;
   occupation: string;
   education: string;
   hobby: string;
   bio: string;
 };

在子组件中,可能只用到了其中一部分的类型,此时可以使用Pick类型将需要的部分分拣出来

 type UserBasicInfo = Pick<User, 'name' | 'age' | 'email'>;
 ​
 const userBasicInfo: UserBasicInfo = {
   name: 'John Doe',
   age: 30,
   email: 'john.doe@example.com'
 };

反之,如果使用到了大部分类型,只有数个类型需要移除,就可以使用Omit类型来减少一些代码量

 type UserDetailedInfo = Omit<User, 'name' | 'age' | 'email'>;
 ​
 const userDetailedInfo: UserDetailedInfo = {
   phone: '1234567890',
   address: '123 Main St',
   gender: 'male',
   occupation: 'developer',
   education: 'Bachelor',
   hobby: 'reading',
   bio: 'A passionate developer'
 };

集合类型差集Exclude和交集Extract.

Exclude差集,能够从一个类型中移除另一个类型中也存在的部分

 type UserProps = 'name' | 'age' | 'email' | 'phone' | 'address';
 type RequiredUserProps = 'name' | 'email';
 ​
 // OptionalUserProps = UserProps - RequiredUserProps
 type OptionalUserProps = Exclude<UserProps, RequiredUserProps>;
 ​
 const optionalUserProps: OptionalUserProps = 'age' | 'phone' | 'address';

Extract则用于提取另一个类型中也存在的部分,即交集

 type UserProps = 'name' | 'age' | 'email' | 'phone' | 'address';
 type RequiredUserProps = 'name' | 'email';
 ​
 type RequiredUserPropsOnly = Extract<UserProps, RequiredUserProps>;
 ​
 const requiredUserPropsOnly: RequiredUserPropsOnly = 'name' | 'email';

函数类型也是一个能够被工具类型处理的重要部分,内置工具类型中提供了 Parameters 和 ReturnType 这两个类型来提取函数的参数类型与返回值类型。

 type Add = (x: number, y: number) => number;
 ​
 type AddParams = Parameters<Add>; // [number, number] 类型
 type AddResult = ReturnType<Add>; // number 类型
 ​
 const addParams: AddParams = [1, 2];
 const addResult: AddResult = 3;
 ​

typeof类型查询操作符

 const addHandler = (x: number, y: number) => x + y;
 ​
 type Add = typeof addHandler; // (x: number, y: number) => number;
 ​
 type AddParams = Parameters<Add>; // [number, number] 类型
 type AddResult = ReturnType<Add>; // number 类型
 ​
 const addParams: AddParams = [1, 2];
 const addResult: AddResult = 3;
 ​

对于异步函数类型,提取出的返回值类型是一个 Promise<string>,TypeScript 中准备了Awaited类型用于解决这样的问题

 const promise = new Promise<string>((resolve) => {
   setTimeout(() => {
     resolve("Hello, World!");
   }, 1000);
 });
 ​
 type PromiseInput = Promise<string>;
 type AwaitedPromiseInput = Awaited<PromiseInput>; // string

可以直接嵌套在 ReturnType 内部使用:

 // 定义一个函数,该函数返回一个 Promise 对象
 async function getPromise() {
   return new Promise<string>((resolve) => {
     setTimeout(() => {
       resolve("Hello, World!");
     }, 1000);
   });
 }
 ​
 type Result = Awaited<ReturnType<typeof getPromise>>; // string 类型
 ​

特殊属性标记为可选的场景通常使用Pick/Omit进行解决。

TS的模版字符串类型

两个反引号和${}组合成为了插槽表达式,可以同时进行变量/计算操作

typescript使用模版字符串可以提供更为精确的字符串类型结构描述

 type Version = `${number}.${number}.${number}`;
 ​
 const v1: Version = '1.1.0';
 const v2: Version = '1.0'; // 报错:类型 "1.0" 不能赋值给类型 `${number}.${number}.${number}`
 const v3: Version = 'a.0.0'; // 报错:类型 "a.0" 不能赋值给类型 `${number}.${number}.${number}`
 ​

类型别名的函数式用法和模版字符串配合

 type SayHello<T extends string | number> = `Hello ${T}`;
 ​
 type Greet1 = SayHello<"linbudu">; // "Hello linbudu"
 type Greet2 = SayHello<599>; // "Hello 599"

模板字符串类型的诞生不仅实现了字面量类型的拼接,还有一个重要的能力是其自动分发的特性

 type SKU = `${Brand}-latest`; // "iphone-latest" | "xiaomi-latest" | "honor-latest"
 ​
 // 多个插槽都被传入了联合类型
 type Brand = 'iphone' | 'xiaomi' | 'honor';
 type Memory = '16G' | '64G';
 type ItemType = 'official' | 'second-hand';
 ​
 type SKU = `${Brand}-${Memory}-${ItemType}`;
 ​
 // 经过组合排列,得到12种情况

模板插槽进行排列组合在开发中可以事半功倍。

TSConfig配置

tsconfig的配置按照能力来划分,可以分为产物控制、输入与输出控制、类型声明、代码检查几大类。

产物控制

如果target 指定了一个版本,比如 es5,但又希望使用 es6 中才有的 Promise 语法,此时就需要在 lib 配置项中新增 'es2015.promise',来告诉 TypeScript 目标环境中需要启用这个能力:

 {
   "compilerOptions": {
     "lib": ["ES2015"],
     "target": "ES5"
   }
 }
 ​

输入控制

 {
   "compilerOptions": {
     "target": "es5",
     "module": "commonjs",
     "outDir": "dist",
     "strict": true
   },
   "include": [  // 需要包含的文件
     "src/**/*"
   ],
   "exclude": [  // exclude 选项,剔除掉已经被 include 进去的文件
     "src/generated",
     "**/*.spec.ts"
   ]
 }
 ​

类型声明

 {
   "compilerOptions": {
     "types": ["node", "jest", "react"],
   }
 }
 ​

以上配置会加载 @types/node@types/jest@types/react 这几个类型定义包。

最后是检查相关的配置,即你看到的 no-XXX 格式的规则,简要介绍下其中主要的部分:

  • noImplicitAny,当 TypeScript 无法推断出你这个变量或者参数到底是什么类型时,它只能默默给一个 any 类型。如果项目维护地还比较认真,可以启用这个配置,来检查看看代码里有没有什么地方是遗漏了类型标注的。
  • noUnusedLocals 与 noUnusedParameters,类似于 ESLint 中的 no-unused-var,它会检查代码中是否有声明了但没有被使用的变量/函数。是否开启同样取决于你对项目质量的要求,毕竟正常情况下项目中其实不应该出现定义了但没有消费的变量,这可能就意味着哪里的逻辑出错了。
  • noImplicitReturns,启用这个配置项会要求你的函数中所有分支代码块都必须有显示的 return 语句,知道 JavaScript 中不写 return (即这里的 Implicit Returns)和只写一个简单的 return 的效果是完全一致的,但从类型层面来说却不一致,它标志着你到底是没有返回值还是返回了一个 undefined 类型的值。因此,启用这个配置项可以让你确保函数中所有的分支都有一个有效的 return 语句,在那些分支层层嵌套的情况下尤其好用。

TS与React的配合

创建react+ts项目

 npx reate-react-app

创建的项目结构:

 ├── index.html
 ├── package.json
 ├── public
 │   └── react.svg
 ├── src
 │   ├── App.css
 │   ├── App.tsx
 │   ├── assets
 │   │   └── react.svg
 │   ├── index.css
 │   ├── main.tsx
 │   └── vite-env.d.ts
 ├── tsconfig.json
 ├── tsconfig.node.json

可以使用 React.FC 类型来描述一个函数式组件的类型,并通过它为属性类型预留的泛型坑位,来描述这个组件的属性类型:

 import * as React from 'react';
 ​
 interface HomeProps {
   owner: string;
 }
 ​
 const Home: React.FC<HomeProps> = ({ owner }) => {
   return <>Home of {owner}</>;
 };
 ​
 const App1: React.FC = () => {
   // √
   return <Home owner='me' />;
 };
 ​
 const App2: React.FC = () => {
   // X 不能将类型“number”分配给类型“string”。
   return <Home owner={599} />;
 };

TS的NPM包的开发

nodemon的安装与配置

 pnpm i nodemon -g

在package.json中进行配置:

 {
   "nodemonConfig": {
     "delay": 500,
     "env": {
       "NODE_ENV": "development"
     },
     "execMap": {
       "ts": "esno",
       "js": "node"
     },
     "ignore": ["node_modules"],
     "verbose": true
   },
 }

常用的tsconfig.json配置

 {
   "compilerOptions": {
     "target": "ES2018",
     "module": "ES2015",
     "types": [],
     "outDir": "dist", // 输出到 dist 目录下
     "skipLibCheck": true,
     "moduleResolution": "node",
     "strictNullChecks": true, // 开启严格检查
     "declaration": true, // 输出声明文件
     "strict": true,
     "noImplicitAny": true,
     "noImplicitReturns": true,
     "noUnusedParameters": false,
     "noUnusedLocals": false,
     "esModuleInterop": true, // ESM 与 CJS 相关,推荐开启来解决大部分问题
     "allowSyntheticDefaultImports": true, // ESM 与 CJS 相关,推荐开启来解决大部分问题
     "baseUrl": "."
   }
 }

指定入口文件:

 {
   "main": "./dist/index.js"
 }
 ​

使用 types 字段来指定你的类型声明入口:

 {
   "main": "./dist/index.js",
   "types": "./dist/index.d.ts",
 }
 ​

基本的发布方式:

 {
   "scripts": {
     "build": "tsc",
     "prepublish": "npm run build"
   }
 }
 ​
 // 在执行 npm publish 时,npm 会自动执行一次构建工作

完整的package.json配置

 {
   "name": "your-lib",
   "version": "0.1.0",
   "main": "./dist/index.js",
   "types": "./dist/index.d.ts",
   "scripts": {
     "build": "tsc",
     "prepublish": "npm run build"
   },
   "nodemonConfig": {
     "delay": 500,
     "env": {
       "NODE_ENV": "development"
     },
     "execMap": {
       "ts": "esno",
       "js": "node"
     },
     "ignore": ["node_modules"],
     "verbose": true
   },
   "dependencies": { },
   "devDependencies": { },
   "publishConfig": {
     "access": "public",
     "registry": "https://registry.npmjs.org/"
   }
 }

从JS项目迁移到TS项目

  • 首先,基于项目规模,设计迁移顺序。
  • 谨记,不要发生逻辑重构,只做类型的补充。不要发生技术栈的替换,只做类型包的补充。
  • 编码工作的第一步,一定是尽可能补充类型定义。
  • 先迁移通用工具方法,再迁移前端组件。
  • 通用类型定义的沉淀

小结:选择合适的迁移方案、避免发生逻辑改动、优先补充类型定义与完善工具方法以及有意识地沉淀通用的类型定义。

TS的学习方式

最好的方式=多写+多总结——没有人是第一天学习 TypeScript 就完成通关,对各种类型工具信手拈来如臂使指的。我们的起跑线都一样,都是在类型报错-Google查找解决方案-发现不适用自己的情况-再次查找或尝试自己动手调整-成功解决-技能点+1-再次发现类型报错这么个不断重复的过程里,一点点积累各种类型的使用小技巧,一点点丰富自己的错题本,一点点成为 TypeScript 高手的。

TypeScript = 类型系统 + 工程能力

TypeScript对外的博客:TypeScript DevBlog