[TOC]

基础 · TypeScript 入门教程

  1. 变量声明后,它的类型就确定了。
  2. 定义变量时,如果没有明确的指定类型,那么 ts 会依照类型推论(Type Inference)的规则推断出一个类型
  3. 定义变量时,如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查
let a = 1 // 等效于 let a: number = 1
a = '1' // error

let b // 等效于 let b: any
b = 1 // ok
b = '1' // ok
b.split(' ') // ok

let a = 1,声明变量的同时进行了初始化操作,ts 知道 1 是 number 类型,因此 ts 能够推导出变量 a 的类型是 number。

let b,仅仅声明了变量 b,但是并没有进行初始化操作,ts 不知道变量 b 是什么类型,ts 会将其视作 any 类型。

  1. ts 对于函数的约束,主要体现在:参数、返回值
  2. 调用函数时,输入多余的(或者少于要求的)参数,是不被允许的
function sum1(x: number, y: number): number {
  return x + y;
}
sum1(1, 2, 3);
// error 应有 2 个参数,但获得 3 个。


function sum2(x: number, y: number): number {
  return x + y;
}
sum2(1);
// error 应有 2 个参数,但获得 1 个。
  1. =>
    1. 在 ts 的类型定义中,**=>** 可以用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型
    2. 在 ES6 中 => 是定义箭头函数使用的语法
let sum1 = function (x: number, y: number): number {
  return x + y;
};

let sum2: (x: number, y: number) => number = function (x: number, y: number): number {
  return x + y;
};

interface ISum {
  (x: number, y: number): number;
}

let sum3: ISum = function (x: number, y: number): number {
  return x + y;
};

function sum4(x: number, y: number): number {
  return x + y;
}

function sum5(x: number, y: number) {
  return x + y;
}

let sum6 = (x: number, y: number): number => x + y

// ……

约束函数的写法是非常灵活的,上述每一个 sum 函数的声明,描述的类型约束信息都是一致的 (x: number, y: number) =>number

  1. 函数的可选参数、默认参数
    1. 语法 **?:**
    2. 可选参数必须接在必填参数后面
    3. ts 会将带有默认值的参数视作可选的
    4. 默认参数不必位于必填参数之后
// ok
let sum1 = (x: number, y?: number) => {
  if (y) {
    return x + y;
  } else {
    return x;
  }
}

// error 必选参数不能位于可选参数后。
let sum2 = (x?: number, y: number): number => {
  // ...
}

// ok
let sum3 = (x: number, y: number = 0) => x + y

// ok
let sum4 = (x: number = 0, y: number) => x + y
  1. 函数重载
    1. 重载允许一个函数接受不同数量或类型的参数时,作出不同的处理
    2. 函数重载可以约束函数的调用方式
// 约束函数被调用的方式 1
function combine(input1: number, input2: number): number;
// 约束函数被调用的方式 2
function combine(input1: string, input2: string): string;
function combine(input1: any, input2: any) {
  if (typeof input1 === "number" && typeof input2 === "number") {
    return input1 + input2;
  } else if (typeof input1 === "string" && typeof input2 === "string") {
    return input1.concat(input2);
  }
}

// ok
combine(1, 2) // 3

// ok
combine('1', '2') // 12

// error 没有与此调用匹配的重载。
combine(1, '2')
  1. instanceof
    1. instanceof 是一个二元操作符
    2. instanceof 用于测试构造函数的 prototype 属性是否出现在对象的原型链中的任何位置
    3. 判断对象 a 是否由构造函数 A 创建 a instanceof A
    4. 判断对象 a 是否由类 A 创建 a instanceof A
      1. 在 ts 和 js 中,类是特殊的构造函数
class Dog {
  bark() {
    console.log("Woof!");
  }
}

class Cat {
  meow() {
    console.log("Meow!");
  }
}

function makeSound(animal: Dog | Cat) {
  if (animal instanceof Dog) {
    animal.bark(); // 在这里,ts 知道 animal 是 Dog 类型
  } else if (animal instanceof Cat) {
    animal.meow(); // 在这里,ts 知道 animal 是 Cat 类型
  }
}

const myDog = new Dog();
const myCat = new Cat();

makeSound(myDog); // 输出: Woof!
makeSound(myCat); // 输出: Meow!
  1. 声明文件必需以 .d.ts 为后缀。
  2. 我们通常不会为 .ts 文件手动编写类型声明文件,因为 .ts 文件本身就包含了类型信息。
  3. .d.ts 文件主要用于为纯 js 代码提供类型信息。
  4. 类型声明 .d.ts 文件对 ts 说:请相信我,按照我说的来做,我会告诉你哪些变量是存在的,以及这些变量的类型信息,即便实际运行时这东西不存在,或者类型信息和我描述的不符,那也不是你 ts 的锅,都是编写这个类型声明文件的小老弟没搞清楚类型导致的问题。
  5. 声明文件的常见用途:给那些使用 js 编写的第三方库提供类型声明信息,以获得对应的代码补全、接口提示等功能,让这些 js 库能够更好地被我们的 **.ts** 文件所使用,从而提高代码的质量和开发的效率。
  6. 类型声明 **.d.ts** 文件仅仅为 **ts** 提供类型描述服务,并不会出现在最终的编译结果中。因此,我们只需要在类型声明文件中编写类型信息即可,不要多此一举地在类型声明文件中编写相关实现,这种做法是不被允许的。
  7. 声明文件相关语法
    1. declare var declare let declare const 声明全局变量
    2. declare function 声明全局方法
    3. declare class 声明全局类
    4. declare enum 声明全局枚举类型
    5. declare namespace 声明(含有子属性的)全局对象
    6. interface 和 type 声明全局类型
    7. export 导出变量
    8. export namespace 导出(含有子属性的)对象
    9. export default ES6 默认导出
    10. export = commonjs 导出模块
    11. export as namespace UMD 库声明全局变量
    12. declare global 扩展全局变量
    13. declare module 扩展模块
    14. /// <reference /> 三斜线指令
  8. 声明语句中只能定义类型,切勿在声明语句中定义具体的实现。
  9. 声明全局变量
    1. 可以使用 declare var declare let declare const 来声明全局变量。
    2. declare var 和 declare let 这两种写法可以认为是等效的。
    3. 使用 declare var 和 declare let 声明的变量的值可能会被更改。
    4. 使用 declare const 声明的变量的值被认为是不可变的。
    5. 在实际应用中,尽可能使用 declare let 或 declare const,因为它们与 ES6 和更现代的 js 特性更为一致。
declare var globalVar: string
declare let globalLet: number
declare const GLOBAL_CONST: boolean

// ok
globalVar = "Hello"

// ok
globalLet = 42

// error 无法分配到 "GLOBAL_CONST" ,因为它是常数。
GLOBAL_CONST = true

当你在某个类型声明文件 .d.ts 中声明了 declare var globalVar: string 之后,这就意味着告诉 ts 在全局环境下,存在一个名为 globalVar 的变量,如果这时候你再去定义 globalVar 变量,ts 就会报错,并告诉你这个变量已经被定义过了,无需重复定义。即便实际上并没有定义过 globalVar 变量,但是 ts 还是会按照你在类型声明文件中的类型信息来判断。

  1. declare function 用于声明一个函数
declare function add(x: number, y: number): number

// ok
add(10, 20)

// error
add(10)

// error
add(10, '20')
declare function fetchData(
  options: { url: string },
  callback: (data: string) => void
): void;

// ok
fetchData({ url: 'http://example.com' }, (data) => {
  console.log(data);
});
  1. declare class 声明全局类
declare class Person {
  name: string;
  age: number;

  constructor(name: string, age: number);

  sayHello(): void;
}

// ok
let bob = new Person("Bob", 30);
bob.sayHello();
  1. declare enum 声明枚举类型
declare enum Directions {
  Up,
  Down,
  Left,
  Right,
}

declare enum Color {
  RED = 0,
  GREEN = 1,
  BLUE = 2,
}

// ok
let directions = [
  Directions.Up,
  Directions.Down,
  Directions.Left,
  Directions.Right,
];

// ok
function paintWall(color: Color = Color.BLUE) {
  // ...
}

// ok
paintWall(Color.RED);
  1. namespace
    1. namespace 是 ts 早期时为了解决模块化而创造的关键字,中文称为命名空间。
    2. 由于历史遗留原因,在早期还没有 ES6 的时候,ts 提供了一种模块化方案,使用 module 关键字表示内部模块。但由于后来 ES6 也使用了 module 关键字,ts 为了兼容 ES6,使用 namespace 替代了自己的 module,更名为命名空间。
    3. 随着 ES6 的广泛应用,现在已经不建议再使用 ts 中的 namespace,而推荐使用 ES6 的模块化方案了,故我们不再需要学习 namespace 的使用了。
    4. ts 的 namespace 被淘汰了,但是在声明文件中,declare namespace 还是比较常用的,它用来表示全局变量是一个对象,包含很多子属性。
  2. declare namespace 声明命名空间
    1. 用来表示全局变量是一个对象,包含很多子属性
    2. 命名空间中如果出现只有一个属性的情况,可以采用缩写的形式
    3. 命名空间允许嵌套
    4. 多个同名的 namespace 声明会被自动合并成一个命名空间。这种特性叫做“声明合并”(Declaration Merging)
declare namespace A {
  var a1: number // 声明可变属性 a1
  let a2: number // 声明可变属性 a2
  const a3: string // 声明只读属性 a3

  enum e {
    e1
  }

  function f1(): void // 声明方法 f1
}

// ok
A.f1();

// ok
A.a1 = 1

// ok
A.a2 = 2

// error 无法为“a3”赋值,因为它是只读属性。
A.a3 = '3'

// ok
A.e.e1
declare namespace A {
  function f1(): void;

  namespace subA {
      function f2(): void;
  }
}

// ok
A.f1();

// ok
A.subA.f2();
declare namespace A.subA {
  function f2(): void
}

// 等效
// declare namespace A {
//   namespace subA {
//     function f2(): void
//   }
// }

// ok
A.subA.f2();
declare namespace A {
  function f1(): void;

  namespace subA {
      function f2(): void;
  }
}

// 等效

declare namespace A {
  function f1(): void;
}

declare namespace A {
  namespace subA {
    function f2(): void
  }
}

命名空间可以拆开写,ts 在处理时,会将多个同名的命名空间合并成一个命名空间(即便这些同名的命名空间不存在于同一个类型声明文件中)。这种特性叫做“声明合并”(Declaration Merging)。所以,无论你是在一个 namespace 声明中嵌套子命名空间,还是分开写多个同名的 namespace 声明,最终的效果都是相同的。

至于合并时若出现冲突,ts 会如何处理,暂且不做介绍。如果真的出现冲突,你可能优先需要考虑的是如何修改代码避免冲突,而非思考 ts 合并类型声明文件时对于冲突的处理机制是什么。处理冲突的最简单方法就是改名儿,为其中一个命名空间、函数、变量等提供一个不同的名称即可。

  1. 在声明文件中使用 interface、type
interface Ix {
  a: "1" | "2";
  b: {
    c?: string;
  };
}

type x = {
  a: "1" | "2";
  b: {
    c?: string;
  };
};

// ok
let x1: Ix = {
  a: "1",
  b: {},
};

// ok
let x2: x = {
  a: "1",
  b: {},
};

// ok
let x3: Ix = {
  a: "1",
  b: {
    c: "c",
  },
};
  1. 暴露在最外层的 interface 或 type 会作为全局类型作用于整个项目中,我们应该尽可能的减少全局变量或全局类型的数量。故最好将他们放到 namespace 下
declare namespace MyNamespace {
  interface Ix {
    a: "1" | "2";
    b: {
      c?: string;
    };
  }

  type x = {
    a: "1" | "2";
    b: {
      c?: string;
    };
  };
}

// ok
let x1: MyNamespace.Ix = {
  a: "1",
  b: {},
};

// ok
let x2: MyNamespace.x = {
  a: "1",
  b: {},
};

// ok
let x3: MyNamespace.Ix = {
  a: "1",
  b: {
    c: "c",
  },
};
  1. 当我们在 ts 中,通过 npm install foo 下载一个 npm 包 foo,并通过 import foo from 'foo' 引入 foo 包,若我们在使用 foo 包时,想要获取到 foo 包的相关类型声明信息,以便享有更友好地智能提示,此时可能就需要我们手写声明文件了。不过,在我们尝试给一个 npm 包创建声明文件之前,首先需要看看它的声明文件是否已经存在。一般来说,npm 包的声明文件可能存在于两个地方:
    1. 与该 npm 包绑定在一起。判断依据是 package.json 中有 types 字段,或者有一个 index.d.ts 声明文件。这种模式不需要额外安装其他包,是最为推荐的,所以以后我们自己创建 npm 包的时候,最好也将声明文件与 npm 包绑定在一起。
    2. 发布到 @types 里。我们只需要尝试安装一下对应的 @types 包就知道是否存在该声明文件,安装命令是 npm install @types/foo --save-dev。这种模式一般是由于 npm 包的维护者没有提供声明文件,所以只能由其他人将声明文件发布到 @types 里了。
  2. 假如以上两种方式都没有找到对应的声明文件,那么我们就需要自己为它写声明文件了。由于是通过 import 语句导入的模块,所以声明文件存放的位置也有所约束,一般有两种方案:
    1. 创建一个 node_modules/@types/foo/index.d.ts 文件,存放 foo 模块的声明文件。这种方式不需要额外的配置,但是 node_modules 目录不稳定,代码也没有被保存到仓库中,无法回溯版本,有不小心被删除的风险,故不太建议用这种方案,一般只用作临时测试。
    2. 创建一个 types 目录,专门用来管理自己写的声明文件,将 foo 的声明文件放到 types/foo/index.d.ts 中。这种方式需要配置下 tsconfig.json 中的 paths 和 baseUrl 字段。

目录结构:

/path/to/project
├── src
|  └── index.ts
├── types
|  └── foo
|     └── index.d.ts
└── tsconfig.json

tsconfig.json 内容:

{
  "compilerOptions": {
    "module": "commonjs",
    "baseUrl": "./",
    "paths": {
      "*": ["types/*"]
    }
  }
}

如此配置之后,使用 import foo from "foo" 导入 foo 时,也会去 types 目录下寻找对应的模块声明文件了。

"baseUrl": "./"

  • baseUrl 用于解析非相对模块名称的基目录。
  • "./" 将基路径设置为当前目录,即 tsconfig.json 所在的目录。
  • 当你在 ts 中,使用 import foo from "foo" 这种非相对路径的方式来引入 foo 模块时,ts 会根据 baseUrl 配置的基路径去查 foo。比如在这个配置中,ts 就会基于 tsconfig.json 所在的位置去查找 foo。

"paths": { "*": ["types/*"] }

  • paths 用于设置模块名到文件路径的映射,这是 ts 的一个高级特性,允许你为模块指定特定的路径或别名。
  • "*" 表示匹配任何模块名。意味着当 ts 解析任何模块时,它都会考虑这个路径配置。
  • ["types/*"] 对于任何给定的模块名,ts 将尝试在 types 目录下查找与模块名相匹配的文件。例如,如果有一个模块名为 “myModule”,那么 ts 将会在 types/myModule.ts 中查找它(或 .tsx, .d.ts,取决于其他配置和文件的存在情况)。
  1. npm 包的声明文件主要有以下几种语法:
    1. export 导出变量
    2. export namespace 导出(含有子属性的)对象
    3. export default ES6 默认导出
    4. export = commonjs 导出模块
  2. npm 包的声明文件与全局变量的声明文件有很大区别。在 npm 包的声明文件中,使用 declare 不再会声明一个全局变量,而只会在当前文件中声明一个局部变量。只有在声明文件中使用 export 导出,然后在使用方 import 导入后,才会应用到这些类型声明。
// ./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;
}

// 等效

// declare const name: string;
// declare function getName(): string;
// declare class Animal {
//   constructor(name: string);
//   sayHi(): string;
// }
// declare enum Directions {
//   Up,
//   Down,
//   Left,
//   Right,
// }
// interface Options {
//   data: any;
// }

// export { name, getName, Animal, Directions, Options };
// ./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",
  },
};
  1. 与 declare namespace 类似,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();
  1. 在 ES6 模块系统中,使用 export default 可以导出一个默认值,使用方可以用 import foo from 'foo' 来导入这个默认值。
  2. 只有 function、class 和 interface 可以直接默认导出,其他的变量需要先定义出来,再默认导出
// ./types/foo/index.d.ts
export default function foo(): string;

// ./src/index.ts
import foo from 'foo';
foo();
export default enum Directions {
  // ERROR: Expression expected.
  Up,
  Down,
  Left,
  Right
}
declare enum Directions {
  Up,
  Down,
  Left,
  Right,
}

export default Directions;

针对这种默认导出,我们一般会将导出语句放在整个声明文件的最前面:

export default Directions;

declare enum Directions {
  Up,
  Down,
  Left,
  Right,
}
  1. 在 ts 中,针对 commonjs 模块导出,有多种方式可以导入
    1. 同样使用 commonjs 规范导入 const ... = require('...')
    2. 使用 es module 规范导入 import ... from '...'
    3. 使用 ts 特有的写法导入 import ... = require('...')
// 导出 commonjs
module.exports = foo; // 整体导出
exports.bar = bar; // 单个导出

// 导入 commonjs
const foo = require('foo'); // 整体导入
const bar = require('foo').bar; // 单个导入

// 导入 es module
import * as foo from 'foo'; // 整体导入
import { bar } from 'foo'; // 单个导入

//  ts 特有的写法
import foo = require('foo'); // 整体导入
import bar = require('foo').bar; // 单个导入
  1. export =
    1. 使用 commonjs 规范的库,假如要为它写类型声明文件的话,就需要使用到 export = 这种语法了
    2. 准确地讲,export = 不仅可以用在声明文件中,也可以用在普通的 ts 文件中。
    3. 由于很多第三方库是 commonjs 规范的,所以声明文件也就不得不用到 export = 这种语法了。但是还是需要再强调下,相比与 export =,我们更推荐使用 ES6 标准的 export default 和 export。
// types/foo/index.d.ts

export = foo;

declare function foo(): string;
declare namespace foo {
  const bar: number;
}

需要注意的是,上例中使用了 export = 之后,就不能再单个导出 export { bar } 了。所以我们通过声明合并,使用 declare namespace foo 来将 bar 合并到 foo 里。

实际上,import ... require 和 export = 都是 ts 为了兼容 AMD 规范和 commonjs 规范而创立的新语法,由于并不常用也不推荐使用,所以这里就不详细介绍了,感兴趣的可以看官方文档。

  1. export as namespace
    1. 既可以通过