TypeScript 入门笔记(一)

1,683 阅读11分钟

本文代码运行在 v4.2.3 版本下,并默认为严格模式

简介

本文改进自 TypeScript 入门教程 ,原文代码执行在“非严格模式”下,这儿我修改为“严格模式”下可正常执行的代码,并精简提炼了许多章节。

Hello World

打开 TS 在线编辑器,执行代码:

function fn(param: string) {
    return param
}
console.log(fn('Hello World'))

编译后的 JS 代码:

function fn(param) {
    return param;
}
console.log(fn('Hello World'));

控制台输出:

"Hello World"

基本概念

  • 在 TS 中,使用 : 指定变量的类型。

  • TS 只在编译时对类型进行静态检查,在运行时不会。

  • TS 编译的时候即使报错了,还是会生成编译结果,如果要在报错的时候终止 js 文件的生成,可以在 tsconfig.json 中配置 noEmitOnError 。

基础篇

1. 原始数据类型

JS 的类型分为两种:原始数据类型 和 对象类型。

原始数据类型包括:布尔值、数值、字符串、null、undefined、Symbol 和 BigInt。

前五种在 TS 中的应用:

1.1 布尔值

使用 boolean 定义布尔值类型:

let isDone: boolean = false;
console.log(isDone)

直接调用 Boolean 也返回一个 boolean 类型:

let createdByBoolean: boolean = Boolean(1);

但是 new Boolean() 返回的是一个 Boolean 对象:

let createdByNewBoolean: Boolean = new Boolean(1);

注意:

在 TS 中,boolean 是 JS 中的基本类型,而 Boolean 是 JS 中的构造函数。 其他基本类型(除了null 和 undefined)一样,不再赘述。

1.2 数值

使用 number 定义数值类型:

let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
// ES6 中的二进制表示法
let binaryLiteral: number = 0b1010; // 10
// ES6 中的八进制表示法
let octalLiteral: number = 0o744; // 484
let notANumber: number = NaN;
let infinityNumber: number = Infinity;

编译结果:

let decLiteral = 6;
let hexLiteral = 0xf00d;
// ES6 中的二进制表示法
let binaryLiteral = 0b1010; // 10
// ES6 中的八进制表示法
let octalLiteral = 0o744; // 484
let notANumber = NaN;
let infinityNumber = Infinity;

1.3 字符串

使用 string 定义字符串类型:

let myName: string = 'pany';

1.4 Void

JS 没有 void 的概念,在 TS 中,可以用 void 表示没有任何返回值的函数:

function logName(): void {
    console.log('pany')
}

声明一个 void 类型的变量没有什么用,因为你只能将它赋值为 undefined

let unusable: void = undefined;

1.5 Null 和 Undefined

在 TS 中,可以使用 nullundefined 来定义这两个原始数据类型:

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

2. 任意值(Any)

如果是一个普通类型,在赋值过程中改变类型是不被允许的:

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

但如果是 any 类型,则允许被赋值为任意类型:

let myFavoriteNumber: any = 'seven';
myFavoriteNumber = 7;
// 编译成功

在任意值上访问任何属性和方法都是允许的,并且声明一个变量为任意值之后,对它的任何操作,返回的内容的类型都是任意值:

let anyThing: any = 'pany';
anyThing.myName;
anyThing.setName('pany-ang');
// 上面的代码不会出现编译时错误,但会出现运行时错误

变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型:

let something;
something = 'seven';
something = 7;
console.log(something); // 7

3. 类型推论

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

上面的代码等价于:

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

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

如果定义的时候没有赋值,会被推断成any类型,参考上一小节:任意值 any

4. 联合类型

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

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;
// 编译成功

这里的 string | number 的含义是,允许的类型是 string 或者 number

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

function getLength(something: string | number): number {
    return something.length;
    // 编译错误,因为 length 不是共有属性
}

function getString(something: string | number): string {
    return something.toString();
    // 编译成功
}

5. 对象的类型(接口)

在面向对象语言中,接口与是对行为的抽象,而具体如何行动需要由类去实现。

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

// 定义接口
interface Person {
    name: string;
    age: number;
}
// 变量 pany 的形状必须和接口 Person 一致
let pany: Person = {
    name: 'pany',
    age: 25
};

接口的可选属性

interface Person {
    name: string;
    age?: number; // 可选属性 age
}

let pany: Person = {
    name: 'pany'
};
// 编译成功

接口的任意属性

interface Person {
    name: string;
    age?: number;
    [propName: string]: any; // 属性取 string 类型,属性值取 any 类型
}

let pany: Person = {
    name: 'pany',
    gender: 'male'
};

注意1:定义了任意属性且属性取 string 类型(除此之外还有 number 类型,常用于类数组),那么确定属性和可选属性的类型都必须是它的属性值类型的子集

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

let pany: Person = {
    name: 'pany',
    age: 25,
    gender: 'male'
};

// 编译错误
// 因为可选属性 age 的类型(number)不是任意属性的类型(string)的子类型

注意2:一个接口只能有一个任意属性,如果接口中有多个类型的属性,则可以在任意属性中使用联合类型

interface Person {
    name: string;
    age?: number;
    [propName: string]: string | number | undefined;
}

let pany: Person = {
    name: 'pany',
    age: 25,
    gender: 'male'
};

// 编译成功

只读属性

interface Person {
    readonly id: number; // readonly 只读
    name: string;
    age?: number;
    [propName: string]: any;
}

let pany: Person = {
    id: 1,
    name: 'pany',
    gender: 'male'
};

pany.id = 2;

// 编译错误:Cannot assign to 'id' because it is a constant or a read-only property.

注意:只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候

interface Person {
    readonly id: number;
    name: string;
    age?: number;
    [propName: string]: any;
}

let pany: Person = {
    name: 'pany',
    gender: 'male'
};

pany.id = 2;

// 第一处错误: 给 pany 赋值时没有给 id 赋值
// 第二处错误: 不能对 pany.id 赋值,因为它是只读的

6. 数组的类型

6.1 数组的表示方法

最简单的方法是使用「类型 + 方括号」来表示数组:

let fibonacci: number[] = [1, 1, 2, 3, 5];

但不允许出现不同的类型:

let fibonacci: number[] = [1, '1', 2, 3, 5];
// 编译错误: Type 'string' is not assignable to type 'number'.

也可以使用数组泛型 Array<elemType> 来表示数组:

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

也可以用接口描述数组

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

6.2 类数组

类数组不是数组类型,比如 arguments

function sum() {
    let args: number[] = arguments;
}
// 编译错误

arguments 实际上是一个类数组,不能用普通的数组的方式来描述,而应该用接口:

function sum() {
    let args: {
        [index: number]: number;
        length: number;
        callee: Function;
    } = arguments;
}

在这个例子中,我们除了约束当索引的类型是数字时,值的类型必须是数字之外,也约束了它还有 lengthcallee 两个属性。

事实上常用的类数组都有自己的接口定义,如 IArguments , NodeList , HTMLCollection等:

function sum() {
    let args: IArguments = arguments;
}

其中 IArguments 是 TS 中定义好了的类型,它实际上就是:

interface IArguments {
    [index: number]: any;
    length: number;
    callee: Function;
}

7. 函数的类型

7.1 函数的表示方法

函数声明

function sum(x: number, y: number): number {
    return x + y;
}
// sum(1, 2) 成功
// sum(1, 2, 3) 编译错误,因为参数不能多也不能少

函数表达式

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

在 TS 的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。而在 ES6 中,=> 叫做箭头函数。

用接口定义函数形状

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

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

7.2 可选参数

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

注意:可选参数必须放在必需参数后面,换句话说,可选参数后面不允许再出现必需参数

7.3 参数的默认值

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

7.4 剩余参数

用 ... 表示剩余参数,它其实是一个数组:

function push(array: any[], ...items: any[]) {
    items.forEach(function(item) {
        array.push(item);
    });
}

let a: any[] = [];
push(a, 1, 2, 3);

注意:剩余参数只能是最后一个参数

7.5 重载

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

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

如果这段代码你看着很诧异,那么就对了!因为我也觉得这种写法,更多的是为了“增加可读性”......

8. 类型断言

8.1 语法

用来手动指定一个值的类型,语法为:

as 类型

或者

<类型>

第二种形式不能在 tsx 中使用,并且<>的语法也运用在“泛型”中,所有一般采用第一种语法。

8.2 将一个联合类型断言为其中一个类型

之前提到过(4. 联合类型),当 TS 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型中共有的属性或方法。

但是在有的时候,我们确实需要在还不确定类型的时候就访问其中一个类型特有的属性或方法,就需要断言:

interface Cat {
    name: string;
    run(): void;
}
interface Fish {
    name: string;
    swim(): void;
}

function isFish(animal: Cat | Fish) {
    if (typeof (animal as Fish).swim === 'function') {
        return true;
    }
    return false;
}

注意:类型断言只能够「欺骗」TS 编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误。

8.3 将一个父类断言为更加具体的子类

将一个父类断言为更加具体的子类​

上面的文章链接中有一个值得注意的结论:TS 的接口是一个类型,不是一个真正的值,它在编译结果中会被删除,当然就无法使用 instanceof 来做运行时判断。

8.4 将任何一个类型断言为 any

有的时候,我们非常确定这段代码不会出错:

window.foo = 1;

但编译器还是提示我们 window 上不存在 foo 属性。

这时候就需要断言:

(window as any).foo = 1;

它极有可能掩盖了真正的类型错误,所以如果不是非常确定,就不要使用 as any 。

8.5 将 any 断言为一个具体的类型

遇到 any 类型的变量时,如果选择无视它,就会任由它滋生更多的 any。选择改进它,通过类型断言及时的把 any 断言为精确的类型,亡羊补牢,使我们的代码向着高可维护性的目标发展。

// 历史遗留代码返回值为 any
function getCacheData(key: string): any {
    return (window as any).cache[key];
}
// 一个接口
interface Cat {
    name: string;
    run(): void;
}
// 将 any 断言为类型 Cat
const tom = getCacheData('tom') as Cat;
tom.run();

8.6 类型断言的限制

类型断言的限制​

ts.xcatliu.com

  • 联合类型可以被断言为其中一个类型

  • 父类可以被断言为子类

  • 任何类型都可以被断言为 any

  • any 可以被断言为任何类型

  • 要使得 A 能够被断言为 B,只需要 A 兼容 B 或 B 兼容 A 即可

其实前四种情况都是最后一个的特例。

8.7 双重断言

双重断言​

除非迫不得已,千万别用双重断言。

8.8 类型断言 vs 类型转换

类型断言 vs 类型转换​

8.9 类型断言 vs 类型声明

类型断言 vs 类型声明​

类型声明是比类型断言更加严格的,所以为了增加代码的质量,我们最好优先使用类型声明,这也比类型断言的 as 语法更加优雅。

8.10 类型断言 vs 泛型

类型断言 vs 泛型​

9. 声明文件

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

声明文件​

10. 内置对象

JS 中有很多内置对象,可以直接在 TS 中当做定义好了的类型。

10.1 ES 的内置对象

// Boolean、Error、Date、RegExp 等

let b: Boolean = new Boolean(1);
let e: Error = new Error('Error occurred');
let d: Date = new Date();
let r: RegExp = /[a-z]/;

10.2 DOM 和 BOM 的内置对象

// Document、HTMLElement、Event、NodeList 等

let allDiv: NodeList = document.querySelectorAll('div');
document.addEventListener('click', function(e: MouseEvent) {
  // Do something
});

10.3 TS 核心库的定义文件

在使用一些常用的方法的时候,TS 实际上已经帮你做了很多类型判断的工作了:

Math.pow(10, '2');
// 编译错误

因为 Math.pow 必须接受两个 number 参数,其类型定义如下:

interface Math {
    pow(x: number, y: number): number;
}

再举一个 DOM 中的例子:

document.addEventListener('click', function(e) {
    console.log(e.targetCurrent);
});
// 编译错误

addEventListener 在 TS 核心库的定义如下:

interface Document extends Node, GlobalEventHandlers, NodeSelector, DocumentEvent {
    addEventListener(type: string, listener: (ev: MouseEvent) => any, useCapture?: boolean): void;
}

所以 e 被推断成了 MouseEvent,而 MouseEvent 是没有 targetCurrent 属性的,所以报错了。

注意:TS 核心库的定义中不包含 Node.js 部分。

10.4 用 TS 写 Node

Node.js 不是内置对象的一部分,如果想用 TS 写 Node.js,则需要引入第三方声明文件:

npm install @types/node --save-dev

进阶篇

TypeScript 入门笔记(二)