Bootstrap

TypeScript快速入门

快速入门手册

安装

npm install -g typescript

项目初始化

// 项目初始化
npm init -f

// tsconfig 初始化
tsc -init

// 使用 shift + ctrl + B 监视文件 or shift + command + B

tsc -init 是用来初始化 tsconfig.json 文件。

如果一个目录下有 tsconfig.json 文件,那么就意味着这个目录是 ts 项目的根目录。tsconfig.json 文件中指定了用来编译这个项目的根文件和编译选项。

{
  /* 编译选项:可以被忽略,这时编译器会使用默认值 */
  "compilerOptions": {
    "module": "commonjs" /* 生成代码的模板标准 */,
    "noImplicitAny": true /* 不允许隐式的 any 类型 */,
    "removeComments": true /* 删除注释 */,
    "preserveConstEnums": true /* 保留 const 和 enum 声明 */,
    "sourceMap": true /* 生成相应的 .map 文件 */,
    "outDir": "./dist"
  },
  "include": ["src/**/*"] /* 指定编译目录 */,
  "exclude": ["node_modules", "**/*.spec.ts"] /* 指定不编译目录 */
}

compilerOptions 配置说明:

{
  "compilerOptions": {
    /* Basic Options */
    // "incremental": true,                   /* TS编译器在第一次编译之后会生成一个存储编译信息的文件,第二次编译会在第一次的基础上进行增量编译,可以提高编译的速度 */
    "target": "es5",                          /* 目标语言的版本: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
    "module": "commonjs",                     /* 生成代码的模板标准: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
    // "lib": ["DOM", "ES2015", "ScriptHost", "ES2019.Array"], /* TS需要引用的库,即声明文件,es5 默认引用dom、es5、scripthost,如需要使用es的高级版本特性,通常都需要配置,如es8的数组新特性需要引入"ES2019.Array" */
    // "allowJs": true,                       /* 允许编译器编译 JS,JSX 文件 */
    // "checkJs": true,                       /* 允许在JS文件中报错,通常与allowJS一起使用 */
    // "jsx": "preserve",                     /* 在 .tsx 文件里支持 JSX: 'preserve', 'react-native', or 'react'. */
    // "declaration": true,                   /* 生成声明文件,开启后会自动生成声明文件 '.d.ts'. */
    // "declarationMap": true,                /* 为声明文件生成 sourceMap */
    // "sourceMap": true,                     /* 为目标文件生成 sourceMap */
    // "outFile": "./",                       /* 将多个相互依赖的文件生成一个文件,如果以 AMD 作为标准,即开启时应设置 "module": "AMD" */
    // "outDir": "./",                        /* 指定输出目录 */
    // "rootDir": "./",                       /* 指定输出文件目录(用于输出),用于控制输出目录结构 */
    // "composite": true,                     /* Enable project compilation */
    // "tsBuildInfoFile": "./",               /* 增量编译文件的存储位置 */
    // "removeComments": true,                /* 删除注释 */
    // "noEmit": true,                        /* 不输出文件,即编译后不会生成任何 js 文件 */
    // "importHelpers": true,                 /* 通过 tslib 引入 helper 函数,文件必须是模块 */
    // "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
    // "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */

    /* Strict Type-Checking Options */
    "strict": true,                           /* 开启所有严格的类型检查 */
    // "noImplicitAny": true,                 /* 不允许隐式的 any 类型 */
    // "strictNullChecks": true,              /* 不允许把 null、undefined 赋值给其他类型的变量 */
    // "strictFunctionTypes": true,           /* 不允许函数参数双向协变 */
    // "strictBindCallApply": true,           /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
    // "strictPropertyInitialization": true,  /* 类的实例属性必须初始化 */
    // "noImplicitThis": true,                /* 不允许 this 有隐式的 any 类型 */
    // "alwaysStrict": true,                  /* 在代码中注入 'use strict' */

    /* Additional Checks */
    // "noUnusedLocals": true,                /* 检查只声明、未使用的局部变量(只提示不报错) */
    // "noUnusedParameters": true,            /* 检查未使用的函数参数(只提示不报错) */
    // "noImplicitReturns": true,             /* 每个分支都会有返回值 */
    // "noFallthroughCasesInSwitch": true,    /* 防止 switch 语句贯穿(即如果没有 break 语句后面不会执行) */

    /* Module Resolution Options */
    // "moduleResolution": "node",            /* 模块解析策略,ts 默认用 node 的解析策略,即相对的方式导入 */
    // "baseUrl": "./",                       /* 解析非相对模块的基地址,默认是当前目录 */
    // "paths": {},                           /* 路径映射,相对于 baseUrl */
    // "rootDirs": [],                        /* 将多个目录放在一个虚拟目录下,用于运行时,即编译后引入文件的位置可能发生变化,这也设置可以虚拟 src 和 out 在同一个目录下,不用再去改变路径也不会报错 */
    // "typeRoots": [],                       /* 声明文件目录,默认时 node_modules/@types */
    // "types": [],                           /* 加载的声明文件包 */
    // "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
    "esModuleInterop": true,                  /* 允许 export = 导出,由import from 导入 */
    // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */
    // "allowUmdGlobalAccess": true,          /* 允许在模块中全局变量的方式访问 umd 模块 */

    /* Source Map Options */
    // "sourceRoot": "",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */
    // "mapRoot": "",                         /* Specify the location where debugger should locate map files instead of generated locations. */
    // "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */
    // "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */

    /* Experimental Options */
    // "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */
    // "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */

    /* Advanced Options */
    "forceConsistentCasingInFileNames": true  /* Disallow inconsistently-cased references to the same file. */
  }
}

以上是 tsconfig.json 文件中的一些常见配置项

基本数据类型

TypeScript 支持与 JavaScript 几乎相同的数据类型,此外还提供了实用的枚举类型方便我们使用。

boolean

let isDone: boolean = false;

number

JavaScript 一样,TypeScript 里的所有数字都是浮点数。 这些浮点数的类型是 number。 除了支持十进制和十六进制字面量,TypeScript 还支持 ECMAScript 2015 中引入的二进制和八进制字面量。

let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
let binaryLiteral: number = 0b1010;
let octalLiteral: number = 0o744;

string

JavaScript 一样,可以使用双引号( ")或单引号(')表示字符串。

let name: string = "bob";
name = "smith";

同样也可以使用 字符串模板

let name: string = `Gene`;
let age: number = 37;
let sentence: string = `Hello, my name is ${ name }.

I'll be ${ age + 1 } years old next month.`;

array

有两种方式定义数组,第一种,在数组元素类型后面使用 []

let list: number[] = [1, 2, 3];

第二种,使用数组泛型,Array<元素类型>

let list: Array<number> = [1, 2, 3];

Tuple

Tuple 类型也是一个数组,我们可以用它来表示一个已知元素数量元素类型的数组。 比如,你可以定义一对值分别为 stringnumber类型的元组。

// Declare a tuple type
let x: [string, number];	
// Initialize it
x = ['hello', 10]; // OK
// Initialize it incorrectly
x = [10, 'hello']; // Error

当访问一个已知索引的元素,会得到正确的类型:

console.log(x[0].substr(1)); // OK
console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'

使用索引进行越界访问:

x[3] = 'world'; // Error, Tuple type '[string, number]' of length '2' has no element at index '2'.

调用数组的方法:

x.push("world"); // OK
x.push(true); // Error, Argument of type 'true' is not assignable to parameter of type 'string | number'.

1、使用索引来访问越界元素,编译器会报错误

2、使用 push 方法新增元素,元素的类型必须满足其联合类型

enum

enum 类型是对 javascript 标准数据类型的一个补充。

enum Days { Sun, Mon, Tue, Wed, Thu, Fri, Sat };

默认情况下,枚举成员从 0 开始赋值,每次递增步长为 1,同时,可以从值到名进行反向映射:

// key -> value
console.log(Days["Sun"] === 0); // true
console.log(Days["Mon"] === 1); // true
console.log(Days["Tue"] === 2); // true
console.log(Days["Sat"] === 6); // true

// value -> key
console.log(Days[0] === "Sun"); // true
console.log(Days[1] === "Mon"); // true
console.log(Days[2] === "Tue"); // true
console.log(Days[6] === "Sat"); // true

同时,我们也可以对枚举项进行手动赋值,当值为 number 类型时,未赋值的枚举项会接着上一个枚举项依次赋值。

enum Days { Sun = 2, Mon, Tue = 5, Wed, Thu, Fri, Sat };

console.log(Days.Sun);  // 2
console.log(Days.Mon);  // 3
console.log(Days.Tue);  // 5
console.log(Days.Wed);  // 6
console.log(Days.Thu);  // 7

如果枚举项的值有重复的话,typescript 不会提示错误,但是通过 value 获取 key 的话,key 是最后一次的枚举项:

enum Days { Sun = 2, Mon = 2, Tue = 1, Wed, Thu, Fri, Sat };
console.log(Days[2]); // Wed

在使用的时候,最好不要出现覆盖的情况。

手动赋值的枚举项可以不是 number 类型,但是,紧跟着的枚举项必须给初始值,否则会报错。

enum Days { Sun = "s", Mon = 2, Tue = 1, Wed, Thu, Fri, Sat };

any

any 表示可以赋值为任意类型。

let myFavoriteNumber: any = 'seven';
myFavoriteNumber = 7;

针对未声明类型的变量,它会被识别为 any

let something;
something = 'seven';
something = 7;

void

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

function bar(): void {}

类型推论

什么是类型推论

以下代码虽然没有指定类型,但是会在编译的时候报错:

let myFavoriteNumber = 'seven';
myFavoriteNumber = 7; // error TS2322: Type '7' is not assignable to type 'string'.

事实上,它等价于:

let myFavoriteNumber: string = 'seven';
myFavoriteNumber = 7;

TypeScript 会在没有明确的指定类型的时候推测出一个类型,这就是类型推论。

如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查

let myFavoriteNumber;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

联合类型

联合类型(Union Types)表示取值可以为多种类型中的一种。

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

联合类型使用 | 分隔每个类型。

访问联合类型的属性和方法

当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法

function getLength(something: string | number): number {
  return something.length;
}
// error TS2339: Property 'length' does not exist on type 'string | number'. Property 'length' does not exist on type 'number'.

上例中,length 不是 stringnumber 的共有属性,所以编译器报错。

访问 stringnumber 的共有属性是没问题的:

function getString(something: string | number): string {
  return something.toString();
}

联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型:

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
console.log(myFavoriteNumber.length);
myFavoriteNumber = 7;
console.log(myFavoriteNumber.length); // error TS2339: Property 'length' does not exist on type 'number'.

在上例中,第 2 行 myFavoriteNumber 被推断成 string 类型,因此访问其 length 属性不会报错。而第 4 行被推断成 number,访问 length 就报错了。

类型断言

类型断言(Type Assertion)可以用来手动指定一个值的类型。

语法

<type> value 

// or

value as type

tsx 中必须使用后面一种。

前面在联合类型中我们提到过,当 Typescript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法

function getLength(something: string | number): number {
  return something.length;
}

// error TS2339: Property 'length' does not exist on type 'string | number'. Property 'length' does not exist on type 'number'.

在这里,我们希望访问 length 属性:

function getLength(something: string | number): number {
  if (something.length) {
    return something.length;
  } else {
    return something.toString().length;
  }
}

// error TS2339: Property 'length' does not exist on type 'string | number'. Property 'length' does not exist on type 'number'.

在上例中,访问 something.length 的时候会报错,因为 length 并不是公共属性。此时,我们就可以使用类型断言,将 something 断言成 string

function getLength(something: string | number): number {
  if ((<string>something).length) {
    return (something as string).length;
  } else {
    return something.toString().length;
  }
}

类型断言不是类型转换,断言成一个联合类型中不存在的类型是不允许的

function toBoolean(something: string | number): boolean {
  return <boolean>something;
}

// error TS2352: Conversion of type 'string | number' to type 'boolean' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. Type 'number' is not comparable to type 'boolean'.

类型别名

类型别名用来给一个类型起个新名字,常用于联合类型。

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
  if (typeof n === 'string') {
    return n;
  } else {
    return n();
  }
} 

字符串字面量类型

字符串字面量类型用来约束取值只能是某几个字符串中的一个。

type EventNames = 'click' | 'scroll' | 'mousemove';
function handleEvent(ele: Element | null , event: EventNames) {
  // do something
}

handleEvent(document.querySelector('hello'), 'scroll');
handleEvent(document.querySelector('world'), 'dbclick'); // error TS2345: Argument of type '"dbclick"' is not assignable to parameter of type 'EventNames'.

上例中,我们使用 type 定了一个字符串字面量类型 EventNames,它只能取三种字符串中的一种。

类型别名与字符串字面量类型都是使用 type 进行定义。

函数

声明式函数

一个函数有输入和输出,要在 TypeScript 中对其进行约束,需要把输入和输出都考虑到,其中函数声明的类型定义较简单:

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

输入多余的(或者少于要求的)参数,都是不被允许的。

sum(1, 2, 3); // error TS2554: Expected 2 arguments, but got 3.
sum(1); //Expected 2 arguments, but got 1.

函数表达式

如果要我们现在写一个对函数表达式(Function Expression)的定义,可能会写成这样:

const sum = (x: number, y: number): number => x + y;

这是可以通过编译的,不过事实上,上面的代码只对等号右侧的匿名函数进行类型定义,而等号左边的 sum,是通过赋值操作进行 类型推论 推断出来的。如果我们需要手动给 sum 添加类型,则应该是这样:

const sum: (x: number, y: number) => number = (x: number, y: number): number => x + y;

不要混淆了 TypeScript 中的 =>ES6 中的 =>

TypeScript 的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。

使用接口定义函数类型

我们可以通过接口来定义函数的类型:

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

const sum: ISum = (x, y) => x + y;

可选参数

前面提到,输入多余的(或者少于要求的)参数,是不允许的。那么如何定义可选的参数呢?

与接口中的可选属性类似,我们用 ? 表示可选的参数:

function buildName(firstName: string, lastName?: string) {
  if (lastName) {
    return firstName + ' ' + lastName;
  } else {
    return firstName;
  }
}
let tomcat: string = buildName('Tom', 'Cat');
let tom: string = buildName('Tom');

需要注意的是,可选参数必须接在确定参数后面。换句话说,可选参数后面不允许再出现确定参数

function buildName(firstName?: string, lastName: string) {
  if (firstName) {
    return firstName + ' ' + lastName;
  } else {
    return lastName;
  }
}
// error TS1016: A required parameter cannot follow an optional parameter.

参数默认值

ES6 中,我们允许给函数的参数添加默认值,TypeScript 会将添加了默认值的参数识别为可选参数

function buildName(firstName: string, lastName: string = 'Cat') {
  return firstName + ' ' + lastName;
}

此时就不受「可选参数必须接在必需参数后面」的限制了:

function buildName(firstName: string = 'Tom', lastName: string) {
  return firstName + ' ' + lastName;
}

剩余参数

ES6 中,可以使用 ...rest 的方式获取函数中的剩余参数(rest 参数)

function push(array, ...items) {
  items.forEach(function (item) {
    array.push(item);
  });
}

事实上,items 是一个数组,所以我们可以用数组的类型来定义:

function push<A, B>(array: A[], ...items: B[]): void {
  items.forEach(item => {
    console.log(item);
  })
}

重载

重载允许一个函数接收不同数量或类型的参数时,作出不同的处理。

比如,我们需要实现一个函数 reverse,输入数字 123 时,返回反转的数字 321,输入字符串 hello 时,返回反转的字符串 olleh,利用联合类型,我们可以这样实现:

type Reverse = string | number;

function reverse(x: Reverse): Reverse {
  if (typeof x === "number") {
    return Number(x.toString().split('').reverse().join(''));
  } else {
    return x.split('').reverse().join('');
  }
}

然而这样做有一个缺点,就是不能 精确 的表达,输入数字的时候,返回也是数字,输入字符串的时候,也应该返回字符串。这时,我们可以使用重载定义多个 reverse 函数类型:

function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string) {
  if (typeof x === "number") {
    return Number(x.toString().split('').reverse().join(''));
  } else {
    return x.split('').reverse().join('');
  }
}

以上代码,我们重复多次定义了 reverse 函数,前几次都是函数的定义,最后一次是函数的实现,这时,在编译阶段的提示中,就可以正确的看到前两个提示了。

TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。

接口

typescript 中,我们可以使用 interface 来定义复杂数据类型,用来描述形状或抽象行为。如:

interface IPerson {
  name: string;
  age: number;
  sayName(): void;
}

const p: IPerson = {
  name: "tom",
  age: 21,
  sayName() {
    console.log(this.name);
  }
};

变量 p 的类型是 IPerson,这样就约束了它的数据结构必须和 IPerson 保持一致,多定义和少定义都是不被允许的。

赋值的时候,变量的形状必须和接口的形状保持一致

可选属性

有时,我们希望不要完全匹配接口中的属性,那么可以用可选属性:

interface IPerson {
  name: string;
  age: number;
  gender?: string; // 可选属性
  sayName(): void;
}

const p: IPerson = {
  name: "tom",
  age: 21,
  sayName() {
    console.log(this.name);
  }
};

在进行赋值时, gender 属性是可以不存在的。当然,这时仍然不允许添加接口中未定义的属性。

只读属性

有时候我们希望对象中的一些属性只能在创建的时候被赋值,那么可以用 readonly 定义只读属性:

interface IPerson {
  readonly id: number;	// 只读属性
  name: string;
  age: number;
  gender?: string;
  sayName(): void;
}

只读约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候。 因此,在对象初始化的时候,必须赋值,之后,这个属性就不能再赋值。

const p: IPerson = {
  id: 1,
  name: "tom",
  age: 21,
  sayName() {
    console.log(this.name);
  }
};

const vs readonly:变量用 const,对象属性用 readonly

任意属性

有时候,我们希望一个接口允许有任意属性:

interface IPerson {
  readonly id: number;
  name: string;
  age: number;
  gender?: string;
  sayName(): void;
  [propsName: string]: any; // 任意属性
}

[propsName: string]: any;通过 字符串索引签名 的方式,我们就可以给 IPerson 类型的变量上赋值任意数量的其他类型。 除了固定的属性外,还可以给IPerson添加任意属性

const p: IPerson = {
  id: 1,
  name: "tom",
  age: 21,
  email: "[email protected]", // 任意属性
  phone: 1234567890, // 任意属性
  sayName() {
    console.log(this.name);
  },
};

emailphone 属性没有在 IPerson 中显性定义,但是编译器不会报错,这是因为我们定义了字符串索引签名。

一旦定义字符串索引签名,那么接口中的确定属性和可选属性的类型必须是索引签名类型的子集。

interface IPerson {
    name: string;
    age?: number;
    [propName: string]: string;
}

// Property 'age' of type 'number | undefined' is not assignable to string index type 'string'.ts(2411)
// (property) IPerson.age?: number | undefined

[propName: string]: string;字符串索引签名类型为 string,但是可选属性 agenumber 类型,number 并不是 string 的子集, 因此编译报错。

表示数组

接口除了可以用来描述对象以外,还可以用来描述数组类型,也就是数字索引签名:

interface NumberArray {
  [index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5];

变量 fibonacci 的类型是 NumberArray,如果还想调用数组的方法,则:

interface NumberArray<T> extends Array<T> {
  [index: number]: T;
}
let fibonacci: NumberArray<number> = [1, 1, 2, 3, 5];

表示函数

接口还可以用来描述函数,约束参数的个数,类型以及返回值:

interface ISearchFunc {
  (source: string, subString: string): boolean
}

let mySearch: ISearchFunc = (source, subString) => {
  let result = source.search(subString);
  return result > -1;
}

额外的类型检测

interface ILabelledObj {
  label: string;
}

function printLabel(labelledObj: ILabelledObj) {
  console.log(labelledObj.label);
}
// 对象字面量
printLabel({ size: 10, label: "Size 10 Object" });
// 对象
let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

在编辑器中运行以上代码,你会发现,在类型检测时,对象字面量提示错误,而却对象没有?这里实际上就是我们所说的额外属性检测:

  • 只要传入的对象满足接口的必要条件,那么它就是被允许的
  • 我们并不会将这种方式称之为 xxx 对象实现了 xxx 接口,在 typescript 只说,它满足了这个约束条件
  • 将对象字面量赋值给变量或作为参数传递的时候,它会被特殊对待且经过额外属性检查,如果存在任何目标类型不包含的属性时,就会提示错误。

那么,我们如何绕开额外类型检查?

  • 最简便的方式是使用类型断言
  • 最佳的方式是添加字符串索引签名最后一种:使用变量对象而不是使用字面量对象

三种方式都可以跳过额外类型检测,具体哪种方式好,自己看着办!

修饰符

TypeScript 可以使用三种访问修饰符(Access Modifiers),分别是 publicprivateprotected

  • public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public
  • private 修饰的属性或方法是私有的,不能在声明它的类的外部访问
  • protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的

例子:

class Animal {
  public name: string;
  public constructor(name: string) {
    this.name = name;
  }
}

let a = new Animal('Jack');
console.log(a.name);
a.name = 'Tom';
console.log(a.name);

上面的例子中,name 被设置为 public,所以直接访问实例的 name 属性是允许的。如果希望 name不被外部访问,这时候就可以用 private

class Animal {
  private name: string;
  public constructor(name: string) {
    this.name = name;
  }
}

let a = new Animal('Jack');
console.log(a.name); // error TS2341: Property 'name' is private and only accessible within class 'Animal'.
a.name = 'Tom'; // error TS2341: Property 'name' is private and only accessible within class 'Animal'.
console.log(a.name); // error TS2341: Property 'name' is private and only accessible within class 'Animal'.

使用 private 修饰的属性或方法,在子类中也是不允许访问的:

class Cat extends Animal {  constructor(name: string) {    super(name);    console.log(this.name); // error TS2341: Property 'name' is private and only accessible within class 'Animal'.  }}

如果使用 protected 修饰,则允许在子类中访问:

class Animal {  protected name: string;  public constructor(name: string) {    this.name = name;  }}class Cat extends Animal {  constructor(name: string) {    super(name);    console.log(this.name);  }}

抽象类

abstract 用于定义抽象类和其中的抽象方法,抽象类是不允许被实例化的:

abstract class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  abstract sayHello(): void;
  sayName() {
    console.log(this.name);
  }
}

new Animal("Jack"); // error TS2511: Cannot create an instance of an abstract class.

其次,抽象类中的抽象方法,必须被子类实现:

abstract class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  abstract sayHello(): void;
  sayName() {
    console.log(this.name);
  }
}

class Cat extends Animal {
  sayHello(): void {
    console.log("hello");
  }
}

const cat: Cat = new Cat("Tom");
cat.sayName();	// ok
cat.sayHello();	// ok

类与接口

实现接口

实现(implements)是面向对象中的一个重要概念。一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),用 implements 关键字来实现。这个特性大大提高了面向对象的灵活性。

举例来说,门是一个类,防盗门是门的子类。如果防盗门有一个报警器的功能,我们可以简单的给防盗门添加一个报警方法。这时候如果有另一个类,车,也有报警器的功能,就可以考虑把报警器提取出来,作为一个接口,防盗门和车都去实现它:

interface Alarm {
  alert(): void;
}

class Door {
}

class SecurityDoor extends Door implements Alarm {
  alert() {
    console.log('SecurityDoor alert');
  }
}

class Car implements Alarm {
  alert() {
    console.log('Car alert');
  }
}

一个类可以实现多个接口:

interface Alarm {  alert(): void;}interface Light {  lightOn(): void;  lightOff(): void;}class Car implements Alarm, Light {  alert() {    console.log('Car alert');  }  lightOn() {    console.log('Car light on');  }  lightOff() {    console.log('Car light off');  }}

上例中,Car 实现了 AlarmLight 接口,既能报警,也能开关车灯。

接口继承接口

接口与接口之间可以是继承关系:

interface Alarm {  alert(): void;}interface LightableAlarm extends Alarm {  lightOn(): void;  lightOff(): void;}

接口继承类

接口也可以继承类:

class Point {  x: number;  y: number;}interface Point3d extends Point {  z: number;}let point3d: Point3d = { x: 1, y: 2, z: 3 };

混合类型

我们知道,接口可以用来定义一个函数:

interface SearchFunc {    (source: string, subString: string): boolean;}let mySearch: SearchFunc;mySearch = function(source: string, subString: string) {    return source.search(subString) !== -1;}

有时候,一个函数还可以有自己的属性和方法:

interface Counter {
  (start: number): string;
  interval: number;
  reset(): void;
}

function getCounter(): Counter {
  const counter: Counter = start => start.toString();
  counter.interval = 123;
  counter.reset = () => { }
  return counter;
}

let c: Counter = getCounter();
c(10);
c.reset();
c.interval = 20;

泛型

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

简单的例子

首先,我们来实现一个函数 createArray,它可以创建一个指定长度的数组,同时将每一项都填充一个默认值:

type CreateArray = (length: number, value: any) => Array<any>;

let createArray: CreateArray = (length, value) => {
  let result = [];
  for (let i = 0; i < length; i++) {
    result[i] = value;
  }
  return result;
}

createArray(3, 'x'); // ['x', 'x', 'x']

上例中,我们使用了数组泛型来定义返回值的类型。这段代码不会报错,但是一个显而易见的缺陷是,它并没有准确的定义返回值的类型:Array<any>允许数组的每一项都为任意类型。但是我们预期的是,数组中每一项都应该为 value 的类型,这时候,泛型就派上用场了:

type CreateArray = <T>(length: number, value: T) => Array<T>;// 箭头函数const createArray: CreateArray = <T>(length: number, value: T): Array<T> => {  let result: T[] = [];  for (let i = 0; i < length; i++) {    result[i] = value;  }  return result;}// 函数表达式const createArray: CreateArray = function <T>(length: number, value: T): T[]{  let result: T[] = [];  for (let i = 0; i < length; i++) {    result[i] = value;  }  return result;}// 声明式函数function createArray<T>(length: number, value: T): Array<T> {  let result: T[] = [];  for (let i = 0; i < length; i++) {    result[i] = value;  }  return result;}createArray<number>(3, 1); // ['x', 'x', 'x']

在上例中,我们在函数中添加了 <T>,其中 T 用来指代任意输入的类型,在后面的输入 value: T 和输出 Array[T] 中即可使用了。在调用的时候,指定他具体类型为 string, 当然,也可以不手动指定,而让类型推论自动推算出来:

createArray(3, 1); // ['x', 'x', 'x']

多个类型参数

定义泛型的时候,可以次定义多个类型参数:

type Swap = <T, U>(tuple: [T, U]) => [U, T];const swap: Swap = <T, U>([p1, p2]: [T, U]): [U, T] => [p2, p1];const result = swap([1, "2"]);

在上例中,我们定义了一个 swap 函数,用来交换输入的 tuple

泛型约束

在函数内部使用泛型变量的时候, 由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法:

function loggingIdentity<T>(arg: T): T {  console.log(arg.length);  return arg;}// error TS2339: Property 'length' does not exist on type 'T'.

上例中,泛型 T 不一定包含属性 length,所以编译的时候报错了。这时,我们可以对泛型进行约束,只允许这个函数传入包含 length 属性的变量。这就是泛型约束:

interface ILengthwise {  length: number;}function loggingIdentity<T extends ILengthwise>(arg: T): T {  console.log(arg.length);  return arg;}

我们使用 extends 约束了泛型 T 必须符合接口 ILengthwise 的定义,也就是必须包含 length 属性。那么这时,如果调用 loggingIdentity 的时候,传入的 arg 不包含 length,那么在编译阶段就会报错了:

loggingIdentity(7); // error TS2345: Argument of type '7' is not assignable to parameter of type 'ILengthwise'.
loggingIdentity('7'); // OK

多个类型参数之间也可以相互约束:

function copyFields<T extends U, U>(target: T, source: U): T {
  for (let key in source) {
    target[key] = (<T>source)[key];
  }
  return target;
}
let x = { a: 1, b: 2, c: 3, d: 4 };
copyFields(x, { b: 10, d: 20 });

上例中,我们使用了两个类型参数,其中要求 T 继承 U,这样就保证了 U 上不会出现 T 中不存在的字段。

泛型接口

我们可以使用接口的方式来定义一个函数:

interface SearchFunc {
  (source: string, subString: string): boolean;
}

let mySearch: SearchFunc = function (source: string, subString: string) {
  return source.search(subString) !== -1;
}

当然也可以使用含有泛型的接口来定义函数:

interface CreateArrayFunc {
  <T>(length: number, value: T): Array<T>;
}

let createArray: CreateArrayFunc = function <T>(length: number, value: T): Array<T> {
  let result: T[] = [];
  for (let i = 0; i < length; i++) {
    result[i] = value;
  }
  return result;
}

进一步,我们还可以把泛型参数提到接口名上:

interface CreateArrayFunc<T> {  (length: number, value: T): Array<T>;}let createArray: CreateArrayFunc<any> = function <T>(length: number, value: T) {  let result: T[] = [];  for (let i = 0; i < length; i++) {    result[i] = value;  }  return result;}createArray(3, "x");

注意,此时在使用泛型接口的时候,需要定义泛型的类型。

泛型类

与泛型接口类似,泛型也可以用于类的类型定义中:

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;

泛型参数的默认类型

在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用。

function createArray<T = string>(length: number, value: T): Array<T> {  let result: T[] = [];  for (let i = 0; i < length; i++) {    result[i] = value;  }  return result;};

声明文件

我们在开发过程中不可避免引入其它第三方的库,虽然通过直接引用,可以调用库提供的方法,但是却无法使用 Typescript 类型检查等特性功能。为了解决这个问题,需要将这些库里的函数去掉方法体,然后导出类型声明,这样就产生了一个描述库和模块信息的声明文件。通过引用这个声明文件,就可以借用 TypeScript 的各种特性来使用库文件了。

声明语句

假如我们想使用第三方库 jquery,一种常见的方式是在 html 中通过 script 标签引入 jquery,然后就可以使用全局变量 $jQuery了。比如,我们通常这样获取一个 idfoo 的元素:

$('#foo');// orjQuery('#foo'); //  error TS2581: Cannot find name '$'.

但是在 ts 中,编译器并不知道 $jQuery 是什么东西。这时,我们需要使用 declare var 来定义它的类型:

declare var $: (selector: string) => any;
declare var jQuery: (selector: string) => any;

上例中,declare var 并没有真的定义一个变量,只是定义了全局变量 $jQuery 的类型,仅仅会用于编译时的检查,在编译结果中会被删除。他的编译结果是:

$("#id");

除了 declare var 之外,还有其他很多种声明语句,稍后介绍。

声明文件

通常我们会把声明语句放到一个单独的文件 jQuery.d.ts 中,这就是声明文件:

// jQuery.d.ts
declare var jQuery: (selector: string) => any;

声明文件必须以 .d.ts 为后缀。一般来说,ts 会解析项目中所有的 *.ts 文件,当然也包含以 .d.ts 结尾的文件。所以,当我们将 jQuery.d.ts 放到项目中时,其他所有的 *.ts 文件就都可以获得 $ 的类型定义了。这里只演示了全局变量这种模式的声明文件,假如是通过模块导入的方式使用第三方库的话,那么引入声明文件又是另一种方式了。

第三方声明文件

当然,jQuery 的声明文件不需要我们定义了,社区已经帮我们定义好了:jquery.d.ts。我们可以下载下来直接使用,但是更推荐的是使用 @types 统一管理第三方库的声明文件。@types 的使用方式很简单,直接用 npm 安装对应的声明模块即可,以 jQuery 为例:

npm install @types/jquery --save-dev

你可以在这个页面搜索需要的声明文件,DefinitelyTyped

自定义声明文件

当一个第三方库没有提供声明文件时,我们就需要手动书写声明文件。前面只介绍了最简单的声明文件内容,而真正书写一个声明文件并不是一件简单的事。以下会详细介绍如何书写声明文件。在不同的场景下,声明文件的内容和使用方式会有所区别。

库的使用场景主要由以下几种:

  • 全局变量:通过 <script> 标签引入第三方库,注入全局变量。
  • npm 包:通过 import ** from xx 导入,符合 ES6 模块化规范。
  • UMD 库:即可以通过 <script> 标签引入,也可以通过 import 引入。
  • 直接扩展全局变量:通过 <script> 标签引入后,改变一个全局变量的结构。String.prototype 新增了一个方法。
  • 通过导入扩展全局变量:通过 import 导入后,可以改变一个全局变量的结构。
全局变量

全局变量是最简单的一种场景,之前的示例中就是通过 <script> 标签引入 jQuery,注入全局变量 $jQuery

使用全局变量的声明文件时,如果是以 npm install @types/xxx --save-dev 安装的,则不需要任何配置。如果是将声明文件直接存放到当前项目中,则建议和其他源码一起放到 src 目录下(或者对应的源码目录下):

├── README.md├── src|  ├── index.ts|  └── jQuery.d.ts└── tsconfig.json

全局变量的声明文件主要由以下几种语法:

  • declare var 声明全局变量
  • declare function 声明全局方法
  • declare class 声明全局类
  • declare enum 声明全局枚举类型
  • declare namespace 声明全局对象
  • interfacetype 声明全局类型
declare var

在所有的的声明语句中,declare var 是最简单的,它可以用来定义一个全局变量的类型。与其类似的是,还有 declare letdeclare const,使用 let 与使用 var 没什么区别,但是使用 const 定义时,表示此时的全局变量是一个常量。一般来说,全局变量都是禁止修改的,所以大部分情况都应该使用 const 而不是 varlet。需要注意的是,在声明语句中只能定义类型,切勿在声明语句中定义具体的值。

// 全局变量foo包含了存在组件总数。
declare var foo: number;
declare function

declare function 用来定义全局函数的类型。jQuery 其实就是一个函数,所以也可以用 function 来定义:

declare function jQuery(selector: string): any;

在函数类型的声明语句中,函数重载也是支持的:

declare function jQuery(selector: string): any;
declare function jQuery(domReadyCallback: () => any): any;
declare class

当全局变量是一个类的时候,我们用 declare class 来定义它的类型:

declare class Animal {
  name: string;
  constructor(name: string);
  sayName(): void
}

同样的,declare class 语句也只能用来定义类型,不能用来定义具体的值,比如定义 sayName 方法的具体实现则会报错:

declare class Animal {
  name: string;
  constructor(name: string);
  sayName() { } // error TS1183: An implementation cannot be declared in ambient contexts.
}
declare enum

使用 declare enum 定义的枚举类型也称作外部枚举,如:

declare enum Directions {    Up,    Down,    Left,    Right}let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];

其中,Directions 是由第三方库定义好的全局变量。

declare namespace

namespace 是 ts 早期为了解决模块化而创造出来的关键字,中文称为命名空间。由于历史原因,在早期还没有 ES6 的时候,ts 提供了一种模块化方案,使用 module 关键字表示内部模块。但由于后来 ES6 也使用了 module 关键字, ts 为了兼容 ES6,使用 namespace 替代了自己的 module,更名为命名空间。随着 ES6 的广泛应用,现在已经不建议再使用 ts 中的 namespace,而推荐使用 ES6 的模块化方案,故我们不再需要学习 namespace 的使用了。namespace 被淘汰了,但是在声明文件中,declare namespace 还是比较常用的,它用来表示全局变量是一个对象,包含很多属性

比如,jQuery 是一个全局变量,它是一个对象,提供了一个 jQuery.ajax 方法可以调用,那么我们就应该使用 declare namespace jQuery 来声明这个拥有多个子属性的全局变量。

declare namespace jQuery {    function ajax(url: string, settings?: any): void;}jQuery.ajax('/api/get_something');

declare namespace 内部,我们直接使用 function ajax 来声明函数,而不是使用 declare function ajax。类似的,也可以使用 constclassenum等语句。

declare namespace jQuery {
  function ajax(url: string, settings?: any): void;
  const version: number;
  class Event {
    blur(eventType: EventType): void
  }
  enum EventType {
    CustomClick
  }
}

jQuery.ajax('/api/get_something');
console.log(jQuery.version);
const e = new jQuery.Event();
e.blur(jQuery.EventType.CustomClick);

在编译之后,declare namespace 内的所有内容都会被删除。

嵌套的命名空间

如果对象拥有更深的层级,则需要使用 namespace 来声明深层的属性类型:

declare namespace jQuery {
  function ajax(url: string, settings?: any): void;
  namespace fn {
    function extend(object: any): void;
  }
}

jQuery.ajax('/api/get_something');
jQuery.fn.extend({
  check: function() {
      return this.each(() => {
          this.checked = true;
      });
  }
});

假如 jQuery 下仅有 fn 这一个属性(没有 ajax 等其他属性或方法),则可以不需要嵌套 namespace

declare namespace jQuery.fn {
  function extend(object: any): void;
}
  
jQuery.fn.extend({
  check: function () {
    return this.each(() => {
      this.checked = true;
    });
  }
});
interface 和 type

除了全局变量之外,有一些类型我们可能也希望暴露出来。在类型声明文件中,我们可以直接使用 interfacetype 来声明一个全局的类型:

interface IAjaxSetting {  method: "GET" | "POST";  data?: any;}declare namespace jQuery {  function ajax(url: string, settings?: IAjaxSetting): void;}

这样的话,在其他文件中也可以使用这个接口了:

const setting: IAjaxSetting = {  method: "GET"};jQuery.ajax('/api/post_something', setting);

typeinterface 类似,不在赘述。

防止命名冲突

暴露在最外层的 interfacetype 会作为全局类型作用于整个项目中,我们应该尽可能的减少全局变量或全局类型的数量,因此,将他们放在 namespace 下:

declare namespace jQuery {  interface IAjaxSetting {    method: "GET" | "POST";    data?: any;  }  function ajax(url: string, settings?: IAjaxSetting): void;}

那么,在使用 IAjaxSetting 接口的时候,也应该加上 jQuery 前缀了:

const setting: jQuery.IAjaxSetting = {  method: "GET"};
声明合并

jQuery 即是一个函数,可以直接调用,也可以是一个对象,拥有子属性,则我们可以组合多个声明语句,他们会不冲突的合并起来:

declare function jQuery(selector: string): any;
declare function jQuery(domReadyCallback: () => any): any;
declare namespace jQuery {
  interface IAjaxSetting {
    method: "GET" | "POST";
    data?: any;
  }
  function ajax(url: string, settings?: IAjaxSetting): void;
}
  
const setting: jQuery.IAjaxSetting = {
  method: "GET"
};
jQuery("#app");
jQuery(() => { });
jQuery.ajax('/api/post_something', setting);
npm 包

一般我们通过 import xxx from "xxx" 导入一个 npm 包,这是符合 ES6 模块规范的。当我们尝试给一个 npm 包创建声明文件之前,首先看看它的声明文件是否存在。一般来说,npm 包的声明文件可能存在于两个地方:

  1. 与该 npm 包绑定在一起。判断依据是 package.json 中有 types 字段,或者有一个 index.d.ts 声明文件。这种模式不需要额外安装其他包,是最为推荐的,所以以后我们自己创建 npm 包的时候,最好也将声明文件与 npm 包绑定在一起。
  2. 发布到了 @types 里只要尝试安装一下对应的包就知道是否存在,安装命令是 npm install @types/xxx --save-dev。这种模式一般是由于 npm 包的维护者没有提供声明文件,所以只能由其他人将声明文件发布到 @types 里了。

假如以上两种方式都没有找到对应的声明文件,那么我们就需要自己为它写声明文件了。由于是通过 import 语句导入的模块,所以声明文件存放的位置也有所约束,一般有两种方案:

  1. 创建一个 node_modules/@types/xxx/index.d.ts 文件,存放 xxx 模块的声明文件。这种方式不需要额外的配置,但是 node_modules 目录不稳定,代码也没有被保存到仓库中,无法回溯版本,有不小心被删除的风险。
  2. 创建一个 types 目录,专门用来管理自己写的声明文件,将 xxx 的声明文件放到 types/xxx/index.d.ts 中。这种方式需要配置下 tsconfig.jsonpathsbaseUrl 字段。

目录结构:

├── README.md
├── src
|  └── index.ts
├── types
|  └── xxx
|     └── index.d.ts
└── tsconfig.json

tsconfig.json

{
    "compilerOptions": {
        "module": "commonjs",	 /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
        "baseUrl": "./",
        "paths": {
            "*" : ["types/*"]
        }
    }
}

如何配置之后,通过 import 导入 xxx 的时候,也会去 types 目录下寻找对应的模块声明文件了。

module 配置可以有很多种选项,不同的选项会影响到导入导出模式。

不管采用了以上两种方式中的哪一种,我都强烈建议大家将书写好的声明文件(通过给原作者发 pr,或者直接提交到 @types 里)发布到开源社区中,享受了这么多社区的优秀的资源,就应该在力所能及的时候给出一些回馈。只有所有人都参与进来,才能让 ts 社区更加繁荣。

export

npm 包的声明文件与全局变量的声明文件有很大的区别。在 npm 包的声明文件中,使用 declare 不再会声明一个全局变量,而只会在当前文件中声明一个局部变量。只有在声明文件中使用 export 导出,然后在引入方 import 导入后,才会应用到这些类型的声明。

export 的语法与非声明文件中的语法类似,区别仅在于声明文件中禁止定义具体的值:

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;
}

对应的导入和使用模块应该是这样:

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'
  }
}
混用 declare 和 export

我们也可以使用 declare 先声明多个变量,最后再用 export 一次性导出。上例的声明文件可以等价的改写成:

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
}

与全局变量的声明文件类似,interface 前是不需要 declare 的。

export namespace

declare namespace 类似,export namespace 也是用来导出一个拥有子属性的对象:

// types/foo/index.d.tsexport namespace foo {  const name: string;  namespace bar {    function baz(): string;  }}// src/main.tsimport { foo } from "./types/foo/index.d"console.log(foo.name);foo.bar.baz();
export default

在 ES6 模块系统中,使用 export default 可以导出一个默认值,引入方可以使用 import foo from "foo",而不是使用 import { foo } from "foo" 来导入这个默认值。在类型声明文件中,export default 用来导出默认值的类型:

// types/foo/index.d.tsexport default function foo(): string;// src/index.tsimport foo from 'foo';foo();

注意,只有 functionclassinterface 可以直接默认导出,其他的变量需要先定义出来,再默认导出:

export default enum Directions {  // error TS1109: Expression expected.  Up,  Down,  Left,  Right}

上例中,export default enum 是错误的语法,需要先使用 declare enum 定义出来,再使用 export default 导出:

export default Directions;declare enum Directions {    Up,  Down,  Left,  Right}

如上,针对这种默认导出,我们一般会将导出语句房子啊整个声明文件的最前面。

;