[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. interfacetype 声明全局类型
    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 vardeclare let 这两种写法可以认为是等效的。
    3. 使用 declare vardeclare let 声明的变量的值可能会被更改。
    4. 使用 declare const 声明的变量的值被认为是不可变的。
    5. 在实际应用中,尽可能使用 declare letdeclare 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. 在声明文件中使用 interfacetype
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. 暴露在最外层的 interfacetype 会作为全局类型作用于整个项目中,我们应该尽可能的减少全局变量或全局类型的数量。故最好将他们放到 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 中的 pathsbaseUrl 字段。

目录结构:

/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. 只有 functionclassinterface 可以直接默认导出,其他的变量需要先定义出来,再默认导出
// ./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 defaultexport
// 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 ... requireexport = 都是 ts 为了兼容 AMD 规范和 commonjs 规范而创立的新语法,由于并不常用也不推荐使用,所以这里就不详细介绍了,感兴趣的可以看官方文档

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