Typescript真香秘笈

本文由 IMWeb首发于 IMWeb 社区网站 imweb.io。点击阅读原文查看 IMWeb 社区更多精彩文章。

1. 前言

2018年Stack Overflow Developer的调研(https://insights.stackoverflow.com/survey/2018/)显示,Type已经成为比Java更受开发者喜爱的编程语言了。

之前我其实对于type没有太多好感,主要是认为其学习成本比较高,写起代码来还要多写很多类型声明,并且会受到静态类型检查的限制,很不自由,与java的设计哲学♂相悖。我相信有很多人也抱持着这样的想法。

然而,最近由于项目需要,学习并使用了一波type,结果。。。

2. Type是什么?

type,顾名思义,就是type + java,也就是加上了类型检查的js。官方对于type的介绍也指出,type是java的超集。纯粹的js语法,在type中是完全兼容的。但是反过来,用type语法编写的代码,却不能在浏览器或者Node环境下直接运行,因为type本身并不是Ecma标准语法。

3. 为什么要使用Type?

很多人坚持java而不愿使用type的一个很大原因是认为java的动态性高,基本不需要考虑类型,而使用type将会大大削弱编码的自由度。但实际上,动态性并不总是那么美好的。至少,现在java的动态性带来了以下三方面的问题:

  1. 代码可读性差,维护成本高。 所谓”动态一时爽,重构火葬场“。缺乏类型声明,对于自己非常熟悉的代码而言,问题不大。但是如果对于新接手或者太长时间没有接触的代码,理解代码的时候需要自行脑补各种字段与类型,如果不幸项目规模比较庞大,也没什么注释,那么你的反应大概会是像这样的: 有了type,每个变量类型与结构一目了然,根本无需自行脑补。搭配编辑器的智能提示,体验可谓舒适,妈妈再也不用担心我拼错字段名了。
  2. 缺乏类型检查,低级错误出现几率高。 人的专注力很难一直都保持高度在线状态,如果没有类型检查,很容易出现一些低级错误。例如给某个string变量赋值数值,或给对象赋值时候缺少了某些必要字段,调用函数时漏传或者错传参数等。这些看起来很低级的错误,虽然大多数情况下在自测或者测试阶段,都能被验出来,但是总会浪费你的一些时间去debug。 使用type,这种情况甚至不会发生,一旦你粗心地赋错值,编辑器立即标红提示,将bug扼杀在摇篮之中。
  3. 类型不确定,运行时解析器需要进行类型推断,存在性能问题。 我们知道java是边解析边执行的,由于类型不确定,所以同一句代码可能需要被多次编译,这就造成性能上的开销。 虽然type现在无法直接解决性能上的问题,因为type最终是编译成java代码的,但是现在已经有从type编译到WebAssembly的工具了:https://github.com/Assembly/assembly。

好了,如果看完了上面的内容,您还是表示对于type不感兴趣,那么后面的内容就可以忽略了哈哈哈。。。

4. Type基础篇 4.1 基础类型

type中的基础类型有:

其中,number、string、boolean、object、null、undefined、symbol都是比较简单的。

例如:

letnum: number= 1; // 声明一个number类型的变量

letstr: string= 'string'; // 声明一个string类型的变量

letbool: boolean= true; // 声明一个boolean类型的变量

letobj: object = { // 声明一个object类型的变量

a: 1,

}

letsyb: symbol = Symbol; // 声明一个symbol类型的变量

null和undefined可以赋值给除了never的其他类型。

如果给变量赋予与其声明类型不兼容的值,就会有报错提示。

例如:

Array 数组类型

在type中,有两种声明数组类型的方式。

方式一:

letarr: Array< number> = [ 1, 2, 3]; // 声明一个数组类型的变量

方式二:

letarr: number[] = [ 1, 2, 3]; // 声明一个数组类型的变量

Tuple 元组类型

元组类似于数组,只不过元组元素的个数和类型都是确定的。

lettuple: [ number, boolean] = [ 0, false];

any类型

当不知道变量的类型时,可以先将其设置为any类型。

设置为any类型后,相当于告诉type编译器跳过这个变量的检查,因此可以访问、设置这个变量的任何属性,或者给这个变量赋任何值,编译器都不会报错。

letfoo: any;

foo.test;

foo = 1;

foo = 'a';

void类型

通常用来声明没有返回值的函数的返回值类型。

functionfoo(): void{

}

never类型

通常用来声明永远不会正常返回的函数的返回值类型:

// 返回never的函数必须存在无法达到的终点

functionerror(message: string): never{

thrownewError(message);

}

// 返回never的函数必须存在无法达到的终点

functioninfiniteLoop(): never{

while( true) {

}

}

never与void的区别便是,void表明函数会正常返回,但是返回值为空。never表示的是函数永远不会正常返回,所以不可能有值。

enum 枚举类型

使用枚举类型可以为一组数值赋予友好的名字。

enumColor {Red, Green, Blue}

letc: Color = Color.Green;

默认情况下,从0开始为元素编号。你也可以手动的指定成员的数值。例如,我们将上面的例子改成从 1开始编号:

enumColor {Red = 1, Green, Blue}

letc: Color = Color.Green;

或者,全部都采用手动赋值:

enumColor {Red = 1, Green = 2, Blue = 4}

letc: Color = Color.Green;

元素类型也支持字符串类型:

enumColor {Red = 'Red', Green = 'Green', Blue = 'Blue'}

letc: Color = Color.Green;

枚举类型提供的一个便利是你可以由枚举的值得到它的名字。例如,我们知道数值为2,但是不确定它映射到Color里的哪个名字,我们可以查找相应的名字:

enumColor {Red = 1, Green, Blue}

letcolorName: string= Color[ 2];

console.log(colorName); // 显示'Green'因为上面代码里它的值是2

4.2 类型断言

有点类似其他强类型语言的强制类型转换,可以将一个值断言成某种类型,编译器不会进行特殊的数据检查和结构,所以需要自己确保断言的准确性。

断言有两种形式,一种为尖括号语法,一种为as语法。

尖括号语法:

letsomeValue: any= "this is a string";

letstrLength: number= (< string>someValue).length;

as语法:

letsomeValue: any= "this is a string";

letstrLength: number= (someValue asstring).length;

在大部分情况下,这两种语法都可以使用,但是在jsx中就只能使用as语法了。

5. Type进阶篇 5.1 函数

函数类型:

函数类型主要声明的是参数和返回值的类型。

functionsum(a: number, b: number): number{

returna + b;

}

约等于

constsum: (numberA: number, numberB: number) =>number= function(a: number, b: number): number{

returna + b;

}

注意到类型定义时参数的名称不一定要与实际函数的名称一致,只要类型兼容即可。

可选参数:

函数参数默认都是必填的,我们也可以使用可选参数。

functionsum(a: number, b: number, c?: number): number{

returnc ? a + b + c : a + b;

}

重载:

java本身是个动态语言。java里函数根据传入不同的参数而返回不同类型的数据是很常见的。

来看个简单但没什么用的例子:

functiondoNothing(input: number): number;

functiondoNothing(input: string): string;

functiondoNothing(input): any{

returninput;

}

console.log(doNothing( 123));

console.log(doNothing( '123'));

当然也可以使用联合类型,但是编译器就无法准确知道返回值的具体类型。

functiondoNothing(input: number| string): number| string{

returninput;

}

console.log(doNothing( '123').length); // 错误:Property 'length' does not exist on type 'string | number'

如果只是单纯参数的个数不同,返回值类型一样,建议使用可选参数而不是重载。

functionsum(a: number, b: number, c?: number) {

returnc ? a + b + c : a + b;

}

5.2 interface 接口

对于一些复杂的对象,需要通过接口来定义其类型。

interfaceSquareConfig {

color: string;

width: number;

}

constsquare: SquareConfig = {

color: 'red', width: 0,

};

可选属性:

默认情况下,每个属性都是不能为空的。如果这么写,将会有报错。

interfaceSquareConfig {

color: string;

width: number;

}

constsquare: SquareConfig = {

color: 'red',

}; // error

可以将用"?"将width标志位可选的属性:

interfaceSquareConfig {

color: string;

width?: number;

}

constsquare: SquareConfig = {

color: 'red',

};

只读属性:

一些对象属性只能在对象刚刚创建的时候修改其值。你可以在属性名前用 readonly来指定只读属性。

interfacePoint {

readonly x: number;

readonly y: number;

}

如果在初始化后试图修改只读属性的值,将会报错。

letp: Point = { x: 10, y: 20};

p.x = 20; // error

函数类型:

接口除了能够描述对象的结构之外,还能描述函数的类型。

interfaceSumFunc {

(a: number, b: number): number;

}

letsum: SumFunc;

sum = (numberA: number, numberB: number) =>{

returnnumberA + numberB;

}

可以看到函数的类型与函数定义时只要参数类型一致即可,参数名不一定要一样。

可索引类型:

可索引类型,实际就是声明对象的索引的类型,与对应值的类型。接口支持两种索引类型,一种是number,一种是string,通过可索引类型可以声明一个数组类型。

interfaceStringArray {

[index: number]: string;

}

letmyArray: StringArray;

myArray = [ "Bob", "Fred"];

letmyStr: string= myArray[ 0];

5.3 class 类

type中的类是java中类的超集,所以如果你了解es6中的class的语法,也不难理解type中class的语法了。

这里主要说下type的class和java的class的不同之处:

  • 只读属性
  • public、private、protected修饰符
  • 抽象类
  • 实现接口

只读属性

类似于接口中的只读属性,只能在类实例初始化的时候赋值。

classUser {

readonly name: string;

constructor( theName: string) {

this.name = theName;

}

}

letuser = newUser( 'Handsome');

user.name = 'Handsomechan'; // 错误!name是只读的

public、private、protected修饰符:

public修饰符表示属性是公开的,可以通过实例去访问该属性。类属性默认都是public属性。

classAnimal {

constructor( publicname: string) {}

}

constanimal = newAnimal( 'tom');

console.log(animal.name); // 'tom'

注意在类的构造函数参数前加上修饰符是一个语法糖,上面的写法等价于:

classAnimal {

publicname: string;

constructor( name: string) {

this.name = name;

}

}

private修饰符表示属性是私有的,只有实例的方法才能访问该属性。

classAnimal {

getName: string{ returnthis.name }

constructor( privatename: string) {}

}

constanimal = newAnimal( 'tom');

console.log(animal.getName); // 'tom'

console.log(animal.name); // Property 'name' is private and only accessible within class 'Animal'.

protected修饰符表示属性是保护属性,只有实例的方法和派生类中的实例方法才能访问到。

classAnimal {

constructor( publicname: string, protectedage: number) {}

}

classCat extendsAnimal {

getAge = : number=>{

returnthis.age;

}

}

constcat = newCat( 'tom', 1);

console.log(cat.getAge); // 1

抽象类:

抽象类做为其它派生类的基类使用。它们一般不会直接被实例化。不同于接口,抽象类可以包含成员的实现细节。 abstract关键字是用于定义抽象类和在抽象类内部定义抽象方法。

abstractclassAnimal {

abstractmakeSound: void; // 抽象方法,必须在派生类中实现

move: void{

console.log( 'roaming the earch...');

}

}

classSheep extendsAnimal {

makeSound {

console.log( 'mie~');

}

}

constanimal = newAnimal; // 错误,抽象类不能直接实例化

constsheep = newSheep;

sheep.makeSound;

sheep.move;

实现接口:

类可以实现一个接口,从而使得类满足这个接口的约束条件。

interfaceClockInterface {

currentTime: Date;

}

classClock implementsClockInterface {

currentTime: Date;

constructor( h: number, m: number) { }

}

5.4 泛型

泛型在强类型语言中很常见,泛型支持在编写代码时候使用类型参数,而不必在一开始确定某种特定的类型。这样做的原因有两个:

  1. 有时候没办法在代码被使用之前知道类型。 例如我们封装了一个request函数,用来发起http请求,返回请求响应字段。 我们在实现request函数的时候,实际上是不能知道响应字段有哪些内容的,因为这跟特定的请求相关。 所以我们将类型确定的任务留给了调用者。// 简单封装了一个request函数 async function request<T>(url: string): Promise<T> { try { const result = await fetch(url).then((response) => { return response.json; }); return result; } catch (e) { console.log('request fail:', e); throw e; } } async function getUserInfo(userId: string): void { const userInfo = await request<{ nickName: string; age: number; }>(`user_info?id=${userId}`) console.log(userInfo); // { nickName: 'xx', age: xx } } getUserInfo('123');
  1. 提高代码的复用率。 如果对于不同类型,代码的操作都是一样的,那么可以使用泛型来提高代码的复用率。// 获取数组或者字符串的长度 function getLen<T extends Array<any> | string>(arg: T): number { return arg ? arg.length : 0; }

当然,您可能觉得这两点在java中都可以轻易做到,根本不需要泛型。是的,泛型本身是搭配强类型食用更佳的,在弱类型下没意义。

在type中,泛型有几种打开方式:

泛型函数:

functionsomeFunction<T>(arg: T) : T{

returnarg;

}

console.log(someFunction< number>( 123)); // 123

泛型类型:

  • interfaceinterface UserInfo<T> { id: T; age: number; } const userInfo: UserInfo<number> = { id: 123, age: 23, }
  • typetype UserInfo<T> = { // 同上 id: T; age: number; } const userInfo: UserInfo<string> = { id: '123', age: 123, }

泛型类:

classUserInfo<T> {

constructor( privateid: T, privateage: number) {};

getId: T {

returnthis.id;

}

}

我们也可以给类型变量加上一些约束。

泛型约束

有时编译器不能确定泛型里面有什么属性,就会出现报错的情况。

functionlogLength<T>(arg: T): T{

console.log(arg.length); // Error: T doesn't have .length

returnarg;

}

解决方法是加上泛型约束。

interfaceTypeWithLength {

length: number,

}

functionlogLength<TextendsTypeWithLength>(arg: T): T{

console.log(arg.length); // ok

returnarg;

}

6. Type高级篇 6.1 高级类型

交叉类型:

交叉类型是将多个类型合并为一个类型。

interfacetypeA {

a?: number,

}

interfacetypeB {

b?: number,

}

letvalue: typeA & typeB = {};

value.a = 1; // ok

value.b = 2; // ok

联合类型:

联合类型表示变量属于联合类型中的某种类型,使用时需要先断言一下。

interfaceTypeA {

a?: number,

}

interfaceTypeB {

b?: number,

}

constvalue: TypeA | TypeB = {};

(<TypeA>value).a = 1; // ok

6.2 类型别名 type

类型别名可以给一个类型起个新名字。类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。可以将type看做存储类型的特殊类型。

typeName = string;

typeNameResolver = =>string;

typeNameOrResolver = Name | NameResolver;

...

6.3 is

is关键字通常组成类型谓词,作为函数的返回值。谓词为 parameterName is Type这种形式, parameterName必须是来自于当前函数签名里的一个参数名。

functionisFish(pet: Fish | Bird): petisFish{

return(<Fish>pet).swim !== undefined;

}

这样的好处是当函数调用后,如果返回true,编译器会将变量的类型锁定为那个具体的类型。

例如:

if(isFish(pet)) {

pet.swim; // 进入这里,编译器认为pet是Fish类型。

} else{

pet.fly; // 进入这里,编译器认为pet是Bird类型。

}

6.4 keyof

keyof为索引类型查询操作符。

interfacePerson {

name: string;

age: number;

}

typeIndexType = keyof Person; // 'name' | 'age'

这样做的好处是使得编译器能够检查到动态属性的类型。

functionpick<T, KextendskeyofT>(obj: T, keys: K[]): T[K][] {

returnkeys.map( key=>obj[key]);

}

console.log(pick(person, [ 'name', 'age'])); // [string, number]

6.5 声明合并

为什么需要声明合并呢?

我们思考一下,在java中,一个对象是不是可能有多重身份。

例如说,一个函数,它可以作为一个普通函数执行,它也可以是一个构造函数。同时,函数本身也是对象,它也可以有自己的属性。

所以这注定了type中的类型声明可能存在的复杂性,需要进行声明的合并。

合并接口

最简单也最常见的声明合并类型是接口合并。从根本上说,合并的机制是把双方的成员放到一个同名的接口里。

interfaceBox {

height: number;

width: number;

}

interfaceBox {

scale: number;

}

letbox: Box = {height: 5, width: 6, scale: 10};

接口的非函数的成员应该是唯一的。如果它们不是唯一的,那么它们必须是相同的类型。如果两个接口中同时声明了同名的非函数成员且它们的类型不同,则编译器会报错。

对于函数成员,每个同名函数声明都会被当成这个函数的一个重载。同时需要注意,当接口 A与后来的接口 A合并时,后面的接口具有更高的优先级。

合并命名空间

Animals声明合并示例:

namespaceAnimals {

exportclassZebra { }

}

namespaceAnimals {

exportinterfaceLegged { numberOfLegs: number; }

exportclassDog { }

}

等同于:

namespaceAnimals {

exportinterfaceLegged { numberOfLegs: number; }

exportclassZebra { }

exportclassDog { }

}

命名空间与类和函数和枚举类型合并

类与命名空间的合并:

classAlbum {

label: Album.AlbumLabel;

}

namespaceAlbum {

exportclassAlbumLabel { }

}

函数与命名空间的合并:

functionbuildLabel(name: string): string{

returnbuildLabel.prefix + name + buildLabel.suffix;

}

namespacebuildLabel {

exportletsuffix = "";

exportletprefix = "Hello, ";

}

console.log(buildLabel( "Sam Smith"));

此外,类与枚举、命名空间与枚举等合并也是可以的,这里不再话下。

6.6 声明文件

声明文件通常是以.d.ts结尾的文件。

如果只有ts、tsx文件,那么其实不需要声明文件。声明文件一般是在用第三方库的时候才会用到,因为第三方库都是js文件,加上声明文件之后,ts的编译器才能知道第三库暴露的方法、属性的类型。

声明语法:

  • declare var、declare let、declare const声明全局变量// src/jQuery.d.ts declare let jQuery: (selector: string) => any;
  • declare function 声明全局方法declare function jQuery(selector: string): any;
  • declare class声明全局类// src/Animal.d.ts declare class Animal { name: string; constructor(name: string); sayHi(): string; }
  • declare enum 声明全局枚举类型declare enum Directions { Up, Down, Left, Right }
  • declare namespace 声明(含有子属性的)全局变量// src/jQuery.d.ts declare namespace jQuery { function ajax(url: string, settings?: any): void; }
  • interface和type声明全局类型interface AjaxSettingsInterface { method?: 'GET' | 'POST' data?: any; } type AjaxSettingsType = { method?: 'GET' | 'POST' data?: any; }
  • export 导出变量 在声明文件中只要用到了export、import就会被视为模块声明文件。模块声明文件中的declare关键字不能声明全局变量。// types/foo/index.d.ts export const name: string; export function getName(): string; export class Animal { constructor(name: string); sayHi(): string; } export enum Directions { Up, Down, Left, Right } export interface Options { data: any; } 对应的导入和使用模块应该是这样:// src/index.ts import { name, getName, Animal, Directions, Options } from 'foo'; console.log(name); let myName = getName; let cat = new Animal('Tom'); let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right]; let options: Options = { data: { name: 'foo' } };
  • export namespace导出(含有子属性的)对象// types/foo/index.d.ts export namespace foo { const name: string; namespace bar { function baz(): string; } } // src/index.ts import { foo } from 'foo'; console.log(foo.name); foo.bar.baz;
  • export default ES6 默认导出// types/foo/index.d.ts export default function foo(): string; // src/index.ts import foo from 'foo'; foo;
  • export = commonjs 导出模块// 整体导出 module.exports = foo; // 单个导出 exports.bar = bar; 在 ts 中,针对这种模块导出,有多种方式可以导入,第一种方式是 const ... = require:// 整体导入 const foo = require('foo'); // 单个导入 const bar = require('foo').bar; 第二种方式是import ... from,注意针对整体导出,需要使用 import * as来导入:// 整体导入 import * as foo from 'foo'; // 单个导入 import { bar } from 'foo'; 第三种方式是 import ... require,这也是 ts 官方推荐的方式:// 整体导入 import foo = require('foo'); // 单个导入 import bar = foo.bar;
  • export as namespace 库声明全局变量 既可以通过 <>标签引入,又可以通过 import导入的库,称为 UMD 库。相比于 npm 包的类型声明文件,我们需要额外声明一个全局变量,为了实现这种方式,ts 提供了一个新语法 export as namespace。 一般使用 export as namespace时,都是先有了 npm 包的声明文件,再基于它添加一条 export as namespace语句,即可将声明好的一个变量声明为全局变量。// types/foo/index.d.ts export as namespace foo; export = foo; declare function foo(): string; declare namespace foo { const bar: number; }
  • declare global 扩展全局变量 使用 declare global可以在 npm 包或者 UMD 库的声明文件中扩展全局变量的类型。// types/foo/index.d.ts declare global { interface String { prependHello: string; } } export {}; // src/index.ts 'bar'.prependHello;
  • declare module扩展模块 如果是需要扩展原有模块的话,需要在类型声明文件中先引用原有模块,再使用 declare module扩展// types/moment-plugin/index.d.ts import * as moment from 'moment'; declare module 'moment' { export function foo(): moment.CalendarKey; } // src/index.ts import * as moment from 'moment'; import 'moment-plugin'; moment.foo;

7. 项目接入

  1. 对于所有的项目,接入ts的第一步就是安装type包,type包中包含tsc编译工具。 npm i type -D
  2. 新建tsconfig.js文件,添加编译配置。 示例:{ "compilerOptions": { "noImplicitAny": false, "target": "es5", "jsx": "react", "allowJs": true, "sourceMap": true, "outDir": "./out", "module": "commonjs", "baseUrl": "./src" }, "include": ["./src/**/*"], "exclude": ["./out"] } include表示要编译的文件所在的位置。 exclude表示哪些位置的文件夹不需要进行编译,可以优化编译速度。 compilerOptions中可以配置编译选项。其中noImplicitAny表示是否禁止隐式声明any,默认为false。 target表示要将ts代码转换成的ECMA目标版本。 jsx可选preserve,react或者react-native。其中preserve表示生成的代码中保留所有jsx标签,react-native等同于preserve,react表示将jsx标签转换成React.函数调用。 allowJs表示是否允许编译js文件,默认为false。 sourceMap表示是否生成sourceMap,默认false。 outDir表示生成的目标文件所在的文件夹。 module指定生成哪个模块系统的代码。 baseUrl表示解析非相对模块名的基准目录。 详细配置参数文档请见:https://www.tslang.cn/docs/handbook/compiler-options.html
  3. 有了tsc和tsconfig,实际上就能将ts文件转换为js文件了。但是我们在实际工程的开发中,一般不会直接用tsc,例如在前端项目中,我们希望能与tsc能和webpack结合起来。在node服务端项目中,我们希望修改文件之后,能够只编译修改过的文件,并且重启服务。下面我将分别介绍前端webpack项目和node项目中接入ts的方法: 前端项目: 好了,非常简单就完成了webpack项目接入ts。 node项目: 在node项目中,可以直接使用tsc编译文件,然后重启服务,但是这样在开发阶段显然是非常低效的。 能不能让node直接执行ts文件呢?这样结合nodemon,就可以很简单地做到修改文件后自动重启服务的效果了。有了ts-node,问题不大! ts-node支持直接运行ts文件,就像用node直接运行js文件一样。它的原理是对node进行了一层封装,在require ts模块的时候,先调用tsc将ts文件编译成js文件,然后再用node执行。 安装ts-node: npm i ts-node -D 运行ts文件:npx ts-node .ts 由于ts-node实际上是在运行阶段对于ts文件进行编译的,所以一般不在生产环境中直接使用ts-node,而是用tsc直接编译一遍,就不会有运行时的编译开销了。
    1. 安装ts-loader: npm i ts-loader -D
    2. 在webpack.config.js中加入相关的配置项module.exports = { mode: "development", devtool: "inline-source-map", // 生成source-map entry: "./app.ts", output: { filename: "bundle.js" }, resolve: { // 添加.ts,.tsx为可解析的文件后缀 extensions: [".ts", ".tsx", ".js", ".jsx"] }, module: { rules: [ // 使用ts-loader解析.ts或者.tsx后缀的文件 { test: /.tsx?$/, loader: "ts-loader" } ] } };
  4. 配置eslint 经过上面的配置之后,如果编译报错会在命令行中有提示,并且在vscode中会对出错的代码进行标红。 如果我们想进一步对于代码风格进行规范化约束,需要配置eslint。实际上有专门针对type的lint工具ts-lint,但是现在并不推荐使用了,因为为了统一ts和js的开发体验,tslint正在逐步地合并到eslint上(https://medium.com/palantir/tslint-in-2019-1a144c2317a9)。
    1. 安装eslint相关依赖 npm i eslint @type-eslint/parser @type-eslint/eslint-plugin -D。 其中: eslint: js代码检测工具。 @type-eslint/parser: 将ts代码解析成ESTree,可以被eslint所识别。 @type-eslint/eslint-plugin: 提供了type相关的eslint规则列表。
    2. 配置.eslintrc.js文件module.exports = { parser: '@type-eslint/parser', // 添加parser extends: [ 'eslint-config-imweb', 'plugin:@type-eslint/recommended', // 引入@type-eslint/recommended规则列表 ], plugins: ['@type-eslint'], // 添加插件 rules: { 'react/sort-comp': 0, 'import/extensions': 0, 'import/order': 0, 'import/prefer-default-export': 0, 'react/no-array-index-key': 1, }, };
    3. 进行了以上的步骤后,发现vscode中还是没有将不符合规则的代码标红。这里的原因是,vscode默认不会对.ts,.tsx后缀的文件进行eslint检查,需要配置一下。在vscode的setting.json文件中加入以下配置: "eslint.validate": [ "java", "javareact", { "language": "typereact", "autoFix": true }, { "language": "type", "autoFix": true } ]
  5. js项目迁移到ts

对于新的项目,自然不用说,直接开搞。但是对于旧项目,怎么迁移呢?

首先第一步还是要先接入type,如前文所述。

接下来就有两种选择:

  1. 如果项目不大,或者下定决心并且有人力重构整个项目,那么可以将项目中的.js、.jsx文件的后缀改成.ts、tsx。不出意外,这时编辑器会疯狂报错,耐心地一个个去解决它们吧。
  2. 如果项目很庞大,无法一下子全部重构,实际上也不妨碍使用ts。 在tsconfig.json文件中配置allowJs: true就可以兼容js。 对于项目中的js文件,有三种处理方式。
    1. 不做任何处理。
    2. 对文件进行改动时候,顺手改成ts文件重构掉。
    3. 给js文件附加.d.ts类型声明文件,特别是一些通用的函数或者组件,这样在ts文件中使用到这些函数或者组件时,编辑器会有只能提示,tsc也会根据声明文件中的类型进行校验。

在ts文件中引入npm安装的模块,可能会出现报错,这是因为tsc找不到该npm包中的类型定义文件,因为有些库是将类型定义文件和源码分离的。

有三种方式解决这一问题:

  1. 如果该库在@types命名空间下已经有可用的类型定义文件,直接用npm安装即可,例如 npm i @types/react -D
  2. 如果该库在@types命名空间下没有可用的类型定义文件,可以自己写一个,然后给该库的作者提个PR。
  3. 本地创建一个全局的类型定义文件,例如global.d.ts。declare module 'lib' { export const test: => void; }

然后在ts文件中就可以使用lib模块中的test方法了。

import{ test } from'lib'; 参考资料:

  • type中文文档:https://www.tslang.cn/docs/home.html
  • type入门教程:https://ts.xcatliu.com/
  • 使用ESLint+Prettier规范React+Type项目:https://zhuanlan.zhihu.com/p/62401626

想要学习web前端的同学,可以参考成都web前端培训班提供的学习大纲;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值