浅谈TypeScript初步使用

290 阅读19分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

一、TypeScript 是什么

TypeScript 是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。

TypeScript 提供最新的和不断发展的 JavaScript 特性,包括那些来自 2015 年的 ECMAScript 和未来的提案中的特性,比如异步功能和 Decorators,以帮助建立健壮的组件。下图显示了 TypeScript 与 ES5、ES2015 和 ES2016 之间的关系:

1.1 TypeScript 与 JavaScript 的区别

TypeScriptJavaScript
JavaScript 的超集用于解决大型项目的代码复杂性一种脚本语言,用于创建动态网页
可以在编译期间发现并纠正错误作为一种解释型语言,只能在运行时发现错误
强类型,支持静态和动态类型弱类型,没有静态类型选项
最终被编译成 JavaScript 代码,使浏览器可以理解可以直接在浏览器中使用
支持模块、泛型和接口不支持模块,泛型或接口
社区的支持仍在增长,而且还不是很大大量的社区支持以及大量文档和解决问题的支持

1.2 为什么要使用 TypeScript

使用js不知道是否能调用这个函数,不知道调用后会返回什么类型?

使用typescript 时编译器会在coding时就检查一些可能出错的地方并进行提示,这样我们可以在代码未运行时就发现一些错误。

www.typescriptlang.org/play?ts=4.6…

二、TypeScript 基础类型

2.1 Boolean 类型

let isDone: boolean = false; 
// ES5:var isDone = false;

2.2 Number 类型

let count: number = 10; 
// ES5:var count = 10; 复制代码

2.3 String 类型

let name: string = "Semliker"; 
// ES5:var name = 'Semlinker'; 复制代码

2.4 Array 类型

let list: number[] = [1, 2, 3]; 
// ES5:var list = [1,2,3]; 
let list: Array<number> = [1, 2, 3]; // Array<number>泛型语法
// ES5:var list = [1,2,3]; 复制代码

2.5 Enum 类型

使用枚举我们可以定义一些带名字的常量。 使用枚举可以清晰地表达意图或创建一组有区别的用例。 TypeScript 支持数字的和基于字符串的枚举。

2.5.1 数字枚举
enum Direction {   
  NORTH,
  SOUTH,
  EAST,
  WEST,
} 
let dir: Direction = Direction.NORTH; 

//js是没有枚举类的 但js可以模拟这一实现。
var Direction = {
  NORTH: 1,
  SOUTH: 2,
  EAST: 3,
  WEST: 4,
}
let dir = Direction.NORTH

默认情况下,NORTH 的初始值为 0,其余的成员会从 1 开始自动增长。换句话说,Direction.SOUTH 的值为 1,Direction.EAST 的值为 2,Direction.WEST 的值为 3。上面的枚举示例代码经过编译后会生成以下代码:

"use strict"; 
var Direction; 
(function (Direction) {   
  Direction[(Direction["NORTH"] = 0)] = "NORTH";
  Direction[(Direction["SOUTH"] = 1)] = "SOUTH";
  Direction[(Direction["EAST"] = 2)] = "EAST";
  Direction[(Direction["WEST"] = 3)] = "WEST";
})(Direction || (Direction = {}));
var dir = Direction.NORTH; 

当然我们也可以设置 NORTH 的初始值,比如:

enum Direction {   
  NORTH = 3,
  SOUTH,
  EAST,
  WEST,
}
2.5.2 字符串枚举

在 TypeScript 2.4 版本,允许我们使用字符串枚举。在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。

enum Direction {   
  NORTH = "NORTH",
  SOUTH = "SOUTH",
  EAST = "EAST",
  WEST = "WEST",
} 

以上代码对于的 ES5 代码如下:

"use strict"; 
var Direction; 
(function (Direction) {     
  Direction["NORTH"] = "NORTH";
  Direction["SOUTH"] = "SOUTH";
  Direction["EAST"] = "EAST";
  Direction["WEST"] = "WEST";
})(Direction || (Direction = {})); 
2.5.3 异构枚举

异构枚举的成员值是数字和字符串的混合:

enum Enum {   
  A,
  B,
  C = "C",
  D = "D", 
  E = 8,
  F,
} 

以上代码对于的 ES5 代码如下:

"use strict";
var Enum; 
(function (Enum) {     
  Enum[Enum["A"] = 0] = "A";
  Enum[Enum["B"] = 1] = "B";
  Enum["C"] = "C";
  Enum["D"] = "D";
  Enum[Enum["E"] = 8] = "E";
  Enum[Enum["F"] = 9] = "F";
})(Enum || (Enum = {})); 

通过观察上述生成的 ES5 代码,我们可以发现数字枚举相对字符串枚举多了 “反向映射”:

console.log(Enum.A) 
//输出:0 
console.log(Enum[0])
//输出:A 

2.6 Any 类型

在 TypeScript 中,任何类型都可以被归为 any 类型。这让 any 类型成为了类型系统的顶级类型(也被称作全局超级类型)。

let notSure: any = 666; 
notSure = "Semlinker";
notSure = false; 

any 类型本质上是类型系统的一个逃逸舱。作为开发者,这给了我们很大的自由:TypeScript 允许我们对 any 类型的值执行任何操作,而无需事先执行任何形式的检查。比如:

let value: any; 
value.foo.bar;// OK
value.trim(); // OK
value(); // OK 
new value(); // OK
value[0][1]; // OK 

在许多场景下,这太宽松了。使用 any 类型,可以很容易地编写类型正确但在运行时有问题的代码。如果我们使用 any 类型,就无法使用 TypeScript 提供的大量的保护机制。为了解决 any 带来的问题,TypeScript 3.0 引入了 unknown 类型。

2.7 Unknown 类型

就像所有类型都可以赋值给 any,所有类型也都可以赋值给 unknown。这使得 unknown 成为 TypeScript 类型系统的另一种顶级类型(另一种是 any)。下面我们来看一下 unknown 类型的使用示例:

let value: unknown;
value = true; // OK 
value = 42; // OK 
value = "Hello World"; // OK 
value = []; // OK 
value = {}; // OK 
value = Math.random; // OK 
value = null; // OK 
value = undefined; // OK 
value = new TypeError(); // OK 
value = Symbol("type"); // OK

对 value 变量的所有赋值都被认为是类型正确的。但是,当我们尝试将类型为 unknown 的值赋值给其他类型的变量时会发生什么?

let value: unknown; 
let value1: unknown = value; // OK 
let value2: any = value; // OK 
let value3: boolean = value; // Error 
let value4: number = value; // Error 
let value5: string = value; // Error 
let value6: object = value; // Error 
let value7: any[] = value; // Error 
let value8: Function = value; // Error 

实际如下图:

unknown 类型只能被赋值给 any 类型和 unknown 类型本身。直观地说,这是有道理的:只有能够保存任意类型值的容器才能保存 unknown 类型的值。毕竟我们不知道变量 value 中存储了什么类型的值。

现在让我们看看当我们尝试对类型为 unknown 的值执行操作时会发生什么。以下是我们在之前 any 章节看过的相同操作:

let value: unknown; 
value.foo.bar; // Error
value.trim(); // Error
value(); // Error
new value(); // Error 
value[0][1]; // Error 

将 value 变量类型设置为 unknown 后,这些操作都不再被认为是类型正确的。通过将 any 类型改变为 unknown 类型,我们已将允许所有更改的默认设置,更改为禁止任何更改。

2.8 Tuple 类型

众所周知,数组一般由同种类型的值组成,但有时我们需要在单个变量中存储不同类型的值,这时候我们就可以使用元组。在 JavaScript 中是没有元组的,元组是 TypeScript 中特有的类型,其工作方式类似于数组。

元组可用于定义具有有限数量的未命名属性的类型。每个属性都有一个关联的类型。使用元组时,必须提供每个属性的值。为了更直观地理解元组的概念,我们来看一个具体的例子:

let tupleType: [string, boolean]; 
tupleType = ["Semlinker", true]; 

在上面代码中,我们定义了一个名为 tupleType 的变量,它的类型是一个类型数组 [string, boolean],然后我们按照正确的类型依次初始化 tupleType 变量。与数组一样,我们可以通过下标来访问元组中的元素:

console.log(tupleType[0]); // Semlinker 
console.log(tupleType[1]); // true

在元组初始化的时候,如果出现类型不匹配的话,比如:

tupleType = [true, "Semlinker"]; 

此时,TypeScript 编译器会提示以下错误信息:

[0]: Type 'true' is not assignable to type 'string'. 
[1]: Type 'string' is not assignable to type 'boolean'. 

很明显是因为类型不匹配导致的。在元组初始化的时候,我们还必须提供每个属性的值,不然也会出现错误,比如:

tupleType = ["Semlinker"]; 

此时,TypeScript 编译器会提示以下错误信息:

2.9 Void 类型

某种程度上来说,void 类型像是与 any 类型相反,它表示没有任何类型。当一个函数没有返回值时,你通常会见到其返回值类型是 void:

// 声明函数返回值为void 
function warnUser(): void {   
  console.log("This is my warning message");
} 

以上代码编译生成的 ES5 代码如下:

"use strict"; 
function warnUser() {   
  console.log("This is my warning message");
}

需要注意的是,声明一个 void 类型的变量没有什么作用,因为它的值只能为 undefined 或 null:

let unusable: void = undefined; 

2.10 Null 和 Undefined 类型

TypeScript 里,undefined 和 null 两者有各自的类型分别为 undefined 和 null。

let u: undefined = undefined; 
let n: null = null; 

默认情况下 null 和 undefined 是所有类型的子类型。 就是说你可以把 null 和 undefined 赋值给 number 类型的变量。然而,如果你指定了 --strictNullChecks 标记, null undefined 只能赋值给 void 和它们各自的类型。

2.11 Never 类型

never 类型表示的是那些永不存在的值的类型。 例如,never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。

// 返回never的函数必须存在无法达到的终点 
function error(message: string): never {   
  throw new Error(message); 
} 
function infiniteLoop(): never {   
  while (true) {}
}

在 TypeScript 中,可以利用 never 类型的特性来实现全面性检查,具体示例如下:

type Foo = string | number;
function controlFlowAnalysisWithNever(foo: Foo) {   
  if (typeof foo === "string") {     
    // 这里 foo 被收窄为 string 类型
  } else if (typeof foo === "number") {     
    // 这里 foo 被收窄为 number 类型
  } else {     
    // foo 在这里是 never     
    const check: never = foo;
  } 
} 

注意在 else 分支里面,我们把收窄为 never 的 foo 赋值给一个显示声明的 never 变量。如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天你的同事修改了 Foo 的类型:

type Foo = string | number | boolean; 

然而他忘记同时修改 controlFlowAnalysisWithNever 方法中的控制流程,这时候 else 分支的 foo 类型会被收窄为 boolean 类型,导致无法赋值给 never 类型,这时就会产生一个编译错误。通过这个方式,我们可以确保

controlFlowAnalysisWithNever 方法总是穷尽了 Foo 的所有可能类型。 通过这个示例,我们可以得出一个结论:使用 never 避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码。

三、TypeScript 断言

有时候你会遇到这样的情况,你会比 TypeScript 更了解某个值的详细信息。通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。

通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。类型断言好比其他语言里的类型转换,但是不进行特殊的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用。

类型断言有两种形式:

3.1 “尖括号” 语法

let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length; 

3.2 as 语法

let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

四、类型

4.1 联合类型

联合类型通常与 null 或 undefined 一起使用:

const sayHello = (name: string | undefined) => {   /* ... */ }; 

例如,这里 name 的类型是 string | undefined 意味着可以将 string 或 undefined 的值传递给sayHello 函数。

sayHello("Semlinker");
sayHello(undefined); 

通过这个示例,你可以凭直觉知道类型 A 和类型 B 联合后的类型是同时接受 A 和 B 值的类型。

4.2 类型别名

类型别名用来给一个类型起个新名字。

type Message = string | string[];
let greet = (message: Message) => {   // ... };

4.3 文字类型

可以直接赋值字符串、文字、数字、布尔来作为类型

function printText(text: string, align: 'left' | 'center' | 'right') {
  /***/
}
printText('xxx', 'center')

function compare(a: number, b: number): 0 | 1 | -1 {
  return a === b ? 0 : a > b ? 1 : -1
}

function handleRequest(url: string, method: 'GET' | 'POST') {}
const req = {
  url: 'http//:xxx',
  method: 'GET'
}
handleRequest(req.url, req.method)

4.4 综合实践

type Admin = {
    nicknameLength: (firstname: string) => number,
    text: string
    (num: number): number,
    tag: 'GOOD' | 'BAD'
}
const fn1: Admin = (num: number) => num > 0 ? 1 : 0;
fn1.text = 'xxx'
fn1.nicknameLength = (firsrname: string) => firsrname.length 
fn1.tag = 'GOOD' as const //as const 可以将字符串转化为文字类型


function handle(fn: Admin): string {
    return fn(1) + fn.text + fn.nicknameLength('xxx') + fn.tag
}

console.log(handle(fn1))

五、TypeScript 接口

在面向对象语言中,接口是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类去实现。

TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。

5.1 对象的形状

interface Person {   
  name: string;
  age: number;
}
let Semlinker: Person = {   
  name: "Semlinker",   
  age: 33,
}; 

5.2 可选 | 只读属性

interface Person {   
  readonly name: string;
  age?: number;
} 

只读属性用于限制只能在对象刚刚创建的时候修改其值。此外 TypeScript 还提供了 ReadonlyArray 类型,它与 Array 相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改。

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error! 
ro.push(5); // error! 
ro.length = 100; // error! 
a = ro; // error!

5.3 接口和类型别名有什么区别

5.3.1 语法不同

两者都可以用来描述对象或函数的类型,但是语法不同。

  • Interface
interface Point {   
  x: number;
  y: number;
} 
interface SetPoint {   
  (x: number, y: number): void;
} 
  • Type
type Point = {   
  x: number;
  y: number;
};
type SetPoint = (x: number, y: number) => void; 
5.3.2 interface可以定义多次,并将被视为单个接口

与类型别名不同,接口可以定义多次,并将被视为单个接口(合并所有声明的成员)。

// These two declarations become: 
interface Point { x: number; y: number; } 
interface Point { x: number; } 
interface Point { y: number; } 
const point: Point = { x: 1, y: 2 }; 
5.3.3 extends方式不同

两者都可以扩展,但是语法又有所不同。此外,请注意接口和类型别名不是互斥的。接口可以扩展类型别名,反之亦然。

Interface extends interface

interface PartialPointX { x: number; } 
interface Point extends PartialPointX { y: number; } 

Type alias extends type alias

type PartialPointX = { x: number; }; 
type Point = PartialPointX & { y: number; }; 

Interface extends type alias

type PartialPointX = { x: number; }; 
interface Point extends PartialPointX { y: number; } 

Type alias extends interface

interface PartialPointX { x: number; }
type Point = PartialPointX & { y: number; }; 
5.3.4 Type可以用于更多的类型

与接口不同,类型别名还可以用于其他类型,如基本类型(原始值)、联合类型、元组。

// primitive 
type Name = string; 
// object 
type PartialPointX = { x: number; };
type PartialPointY = { y: number; };
// union 
type PartialPoint = PartialPointX | PartialPointY;
// tuple
type Data = [number, string];
// dom
let div = document.createElement('div');
type B = typeof div; 
5.3.5 Type可以计算属性,生成映射类型

type 能使用 in 关键字生成映射类型,但 interface 不行。

语法与索引签名的语法类型,内部使用了 for .. in。 具有三个部分:

  • 类型变量 K,它会依次绑定到每个属性。
  • 字符串字面量联合的 Keys,它包含了要迭代的属性名的集合。
  • 属性的结果类型。
type Keys = "firstname" | "surname"
type DudeType = {   
  [key in Keys]: string
}
const test: DudeType = {   
  firstname: "Pawel",
  surname: "Grzybek"
}

// 报错
//interface DudeType2 { //  [key in keys]: string //} 

5.4 动态添加属性

有一些场景,接口给你返回一个对象,但是其中有一些值是很必要,一些又没那么必要的话,如何定义?

//接口方式
interface responseConfig {
  id: number,
  name: string,
  [propsName: string]: any
}
//类型方式
type responseConfig = {
  id: number,
  name: string,
  [propsName: string]: any
}

六、TypeScript 泛型

软件工程中,我们不仅要创建一致的定义良好的 API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。

在像 C# 和 Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。

设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函数参数和函数返回值。

泛型(Generics)是允许同一个函数接受不同类型参数的一种模板。相比于使用 any 类型,使用泛型来创建可复用的组件要更好,因为泛型会保留参数类型。

6.1 泛型接口

interface IsDoubleFn<T> {   
  (arg: T): T;
} 
const handles: IsDoubleFn<number> = (n) => n * 2

6.2 泛型类

class GenericNumber<T> {   
  zeroValue: T;
  add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = (x, y) => x + y; 

6.3 泛型变量

对刚接触 TypeScript 泛型的小伙伴来说,看到 T 和 E,还有 K 和 V 这些泛型变量时,估计会一脸懵逼。其实这些大写字母并没有什么本质的区别,只不过是一个约定好的规范而已。也就是说使用大写字母 A-Z 定义的类型变量都属于泛型,把 T 换成 A,也是一样的。下面我们介绍一下一些常见泛型变量代表的意思:

  • T(Type):表示一个 TypeScript 类型
  • K(Key):表示对象中的键类型
  • V(Value):表示对象中的值类型
  • E(Element):表示元素类型

6.4 结合泛型的映射类型

根据旧的类型创建出新的类型, 我们称之为映射类型

interface TestInterface{
    name:string,
    age:number
}

// 我们可以通过+/-来指定添加还是删除

type OptionalTestInterface<T> = {
  [p in keyof T]+?:T[p]
}

type newTestInterface = OptionalTestInterface<TestInterface>
// type newTestInterface = {
//    name?:string,
//    age?:number
// }

比如我们再加上只读

type OptionalTestInterface<T> = {
 +readonly [p in keyof T]+?:T[p]
}

type newTestInterface = OptionalTestInterface<TestInterface>
// type newTestInterface = {
//   readonly name?:string,
//   readonly age?:number
// }

6.5 泛型案例分析

function map<Input, Output>(arr: Input[], func: (arg:Input) => Output): Output[] {
    return arr.map(func)
}
const arr = map(['1','2'], (n) => parseInt(n))
console.log(arr)

function longest <Type extends { length: number }>(a: Type, b: Type){
    if(a.length >= b.length){
        return a
    } else return b
}

console.log(longest([1,2,3], [1,2]))
console.log(longest('asdasd', 'asd'))
console.log(longest(123, 456456))
//泛型写法
function mylength<T extends (arg: string) => boolean>(func: T){
    return func('123')
}
function mylength(func: (arg: string) => boolean){
    return func('123')
}
mylength((n) => {
    return n ? true : false
})

七、tsconfig.json配置介绍

tsconfig.json 是 TypeScript 项目的配置文件。如果一个目录下存在一个 tsconfig.json 文件,那么往往意味着这个目录就是 TypeScript 项目的根目录。

tsconfig.json 包含 TypeScript 编译的相关配置,通过更改编译配置项,我们可以让 TypeScript 编译出 ES6、ES5、node 的代码。

其中该文件下一些重要的字段如下:

  • files - 设置要编译的文件的名称;
  • include - 设置需要进行编译的文件,支持路径模式匹配;
  • exclude - 设置无需进行编译的文件,支持路径模式匹配;
  • compilerOptions - 设置与编译流程相关的选项。

compilerOptions 选项的字段解析

{
  "compilerOptions": {
    
    /* 基本选项 */
    "target": "es5",                       // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
    "module": "commonjs",                  // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
    "lib": [],                             // 指定要包含在编译中的库文件
    "allowJs": true,                       // 允许编译 javascript 文件
    "checkJs": true,                       // 报告 javascript 文件中的错误
    "jsx": "preserve",                     // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
    "declaration": true,                   // 生成相应的 '.d.ts' 文件
    "sourceMap": true,                     // 生成相应的 '.map' 文件
    "outFile": "./",                       // 将输出文件合并为一个文件
    "outDir": "./",                        // 指定输出目录
    "rootDir": "./",                       // 用来控制输出目录结构 --outDir.
    "removeComments": true,                // 删除编译后的所有的注释
    "noEmit": true,                        // 不生成输出文件
    "importHelpers": true,                 // 从 tslib 导入辅助工具函数
    "isolatedModules": true,               // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似).
    
    /* 严格的类型检查选项 */
    "strict": true,                        // 启用所有严格类型检查选项
    "noImplicitAny": true,                 // 在表达式和声明上有隐含的 any类型时报错
    "strictNullChecks": true,              // 启用严格的 null 检查
    "noImplicitThis": true,                // 当 this 表达式值为 any 类型的时候,生成一个错误
    "alwaysStrict": true,                  // 以严格模式检查每个模块,并在每个文件里加入 'use strict'
    
    /* 额外的检查 */
    "noUnusedLocals": true,                // 有未使用的变量时,抛出错误
    "noUnusedParameters": true,            // 有未使用的参数时,抛出错误
    "noImplicitReturns": true,             // 并不是所有函数里的代码都有返回值时,抛出错误
    "noFallthroughCasesInSwitch": true,    // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)
    
    /* 模块解析选项 */
    "moduleResolution": "node",            // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
    "baseUrl": "./",                       // 用于解析非相对模块名称的基目录
    "paths": {},                           // 模块名到基于 baseUrl 的路径映射的列表
    "rootDirs": [],                        // 根文件夹列表,其组合内容表示项目运行时的结构内容
    "typeRoots": [],                       // 包含类型声明的文件列表
    "types": [],                           // 需要包含的类型声明文件名列表
    "allowSyntheticDefaultImports": true,  // 允许从没有设置默认导出的模块中默认导入。
    
    /* Source Map Options */
    "sourceRoot": "./",                    // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
    "mapRoot": "./",                       // 指定调试器应该找到映射文件而不是生成文件的位置
    "inlineSourceMap": true,               // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
    "inlineSources": true,                 // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性
    
    /* 其他选项 */
    "experimentalDecorators": true,        // 启用装饰器
    "emitDecoratorMetadata": true          // 为装饰器提供元数据的支持
  }
}

更多配置请看官网的配置选项www.tslang.cn/docs/handbo…

八、TypeScript声明文件

8.1 declare 声明

当使用第三方库时,很多三方库不是用 TS 写的,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。

比如,在 TS 中直接使用 Vue,就会报错,

这时,我们可以使用 declare 关键字来定义 Vue 的类型,简单写一个模拟一下,

interface VueOption {
  el: string,
  data: any
}

declare class Vue {
  options: VueOption
  constructor(options: VueOption)
}

const app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

这样就不会报错了,使用 declare 关键字,相当于告诉 TS 编译器,这个变量(Vue)的类型已经在其他地方定义了,你直接拿去用,别报错。

8.2 小实践-自己写个声明文件

写了一个请求小模块 myFetch,代码如下,

function myFetch(url, method, data) {
  return fetch(url, {
      body: data ? JSON.stringify(data) : undefined,
      method
  }).then(res => res.json())
}

myFetch.get = (url) => {
  return myFetch(url, 'GET')
}

myFetch.post = (url, data) => {
  return myFetch(url, 'POST', data)
}

export {
  myFetch
}

现在新项目用了 TS 了,要在新项目中继续用这个 myFetch,你有两种选择:

  • 用 TS 重写 myFetch,新项目引重写的 myFetch
  • 直接引 myFetch ,给它写声明文件

如果选择第二种方案,就可以这么做,

type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'

declare function myFetch<T = any>(url: string, method: HTTPMethod, data?: any): Promise<T>

declare namespace myFetch { // 使用 namespace 来声明对象下的属性和方法
    const get: <T = any>(url: string) => Promise<T> 
    const post: <T = any>(url: string, data: any) => Promise<T>
}

创建一个 types 目录,专门用来管理自己写的声明文件,将 myFetch 的声明文件放到 types 目录中,并在tsconfig.json中配置一下:

然后测试一下,原来导入的js方法已经有了智能提示:

有着优雅的代码提示

或者不用declare声明的形式也可以,把d.ts文件放到跟js文件同级

//index.d.ts
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'

export function myFetch<T = any>(url: string, method: HTTPMethod, data?: any): Promise<T>

export namespace myFetch { // 使用 namespace 来声明对象下的属性和方法
    const get: <T = any>(url: string) => Promise<T> 
    const post: <T = any>(url: string, data: any) => Promise<T>
}
//index.js
function myFetch(url, method, data) {
  return fetch(url, {
      body: data ? JSON.stringify(data) : undefined,
      method
  }).then(res => res.json())
}

myFetch.get = (url) => {
  return myFetch(url, 'GET')
}

myFetch.post = (url, data) => {
  return myFetch(url, 'POST', data)
}

export {
  myFetch
}

使用直接导入就可以识别

最后

以上ts的使用都是比较基础的用法,想用更好肯定要比较多的实践归纳总结出方法。

一些高阶用法没有提及并不是说不重要,因为我也不会。。。

各位有什么ts比较好/高效的用法 可以讨论一下。。。