TypeScript学习笔记(一)(基础部分)

TypeScript学习笔记(一)(基础部分)

预言:为什么学习TypeScript?

2020都来了,TypeScript还不快学习一下

VUE3也使用了TS,现在三大框架都转为TypeScript,Node无力回天,Deno前景明朗,使用V8引擎解析TS。

TS遍地开花,大势所趋。2020肯定迟早都要学的,不如早点儿赶上潮流(😥其实不算早了)

它是javascript的超集,有静态类型检查,清晰函数参数,接口属性,增加了代码的可读性。

入门学习推荐:

TypeScript入门教程文档

技术胖ts视频教程

安装

TypeScript 的命令行工具安装全局:

1
cnpm install -g typescript

helloWorld

创建文件hello.ts

1
2
3
4
5
6
function sayHello(person: string) {
return `hello, ${person}`
}

let user = 'Jason'
console.log(sayHello(user))

终端输入:tsc hello.ts

编译完后,出现文件hello.js

1
2
3
4
5
function sayHello(person) {
return "hello, " + person;
}
var user = 'Jason';
console.log(sayHello(user));

数据类型

boolean(布尔值)

使用 boolean 定义布尔值类型:

1
let isDone: boolean = false;

注意,使用构造函数 Boolean 创造的对象不是布尔值:

1
let createdByNewBoolean: boolean = new Boolean(1);

new Boolean() 返回的是一个 Boolean 对象,boolean 是 JavaScript 中的基本类型,而 Boolean 是 JavaScript 中的构造函数。

number(数值)

使用 number 定义数值类型:

1
2
3
4
5
6
let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
// 二进制
let binaryLiteral: number = 0b1010;
// 八进制
let octalLiteral: number = 0o744;

编译结果:

1
2
3
4
var decLiteral = 6;
var hexLiteral = 0xf00d;
var binaryLiteral = 10;
var octalLiteral = 484;

二进制和八进制,它们会被编译为十进制数字。

为什么tsc不转十六进制?

十和十六进制本身就有,二和八进制是ES6引入的,tsc默认转为ES5,tsc -t ES6 hello.ts转es6就不会改变

image-20200101172413290

string(字符串)

使用 string 定义字符串类型:

1
2
3
4
let myName: string = 'Jason';
let myAge: number = 20;
// 模板字符串
let sentence: string = `Hello, my name is ${myName}.I'll be ${myAge + 1} years old next month.`;

空值

使用 void 表示没有任何返回值的函数:

1
2
3
function alertName(): void {
alert('My name is Jason');
}

使用 nullundefined 来定义这两个原始数据类型:

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

undefinednull 是所有类型的子类型。

1
2
// 这样不会报错
let num: number = undefined;

任意值

any 类型,允许被赋值为任意类型。

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

声明一个变量为任意值之后,对它的任何操作,返回的内容的类型都是任意值

如果申明 变量 未申明 类型 和 值,那么它会被识别为任意值类型

1
2
let something; 
//等价于 let something: any;

类型推论

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

1
2
3
4
let myFavoriteNumber = 'seven';
// 等价于 let myFavoriteNumber: string = 'seven';
myFavoriteNumber = 7;
// 再定义数字类型就会出错 index.ts(2,1): error TS2322: Type 'number' is not assignable

如果申明 ‘变量’ 未申明 ‘类型’ 单定义 ‘值’ ,那么它会自动推测类型

联合类型

表示取值可以为多种类型中的一种,使用 | 分隔每个类型。

例子

1
2
3
let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

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

1
2
3
4
function getLength(something: string | number): number {
return something.length;
}
// 报错 length不是 string 和 number 共有属性

联合类型变量被赋值之后,就可以使用其类型方法


接口

可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义接口
interface Person {
// 只读属性,只能在第一次给 ‘对象’ 创建的时候 赋值
readonly id: number;
name: string;
// 可选属性,该属性可以不存在
age?: number;
// 任意属性,确定属性 和 可选属性 的类型都必须是其类型的子集
//[propName: string]: string; 报错,age?: number;不是string的子集
[propName: string]: any;
}
// 定义变量jason,约束jason的形状必须和Person接口一致,接口首字母大写或I开头
let jason: Person = {
id: 19040108
name: 'Jason
age: 20
//任意属性下的结果
gender: 'male'
};
jason.id = 19040101; //报错,只读属性不能再次赋值

注意只读属性只能在第一次给 ‘对象’ 创建的时候 赋值,不能在外面单独赋值

image-20200102105140825


数组的类型

「类型 + 方括号」表示法

1
2
3
let fibonacci: number[] = [1, 1, 2, 3, 5];
// let fibonacci: number[] = [1, '1', 2, 3, 5]; //出现string报错
// fibonacci.push('8'); 报错,推入string也不行

数组泛型 表示法

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

接口 表示法

常用来表示类数组

1
2
3
4
5
interface NumberArray {
// 只要索引的类型是数字时,那么值的类型必须是数字
[index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5];

类数组

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

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

1
2
3
4
5
6
7
interface IArguments {
// 约束当 索引 的类型是数字时,值的类型是任意类型
[index: number]: any;
// 约束它有length 和callee两个属性
length: number;
callee: Function;
}

函数的类型

函数声明的类型定义:

1
2
3
4
5
function sum(x: number, y: number): number {
return x + y;
}
sum(1);// 报错,输入了过少的参数
sum(1, 2, 3);// 报错,输入了过多的参数

数表达式的定义:

1
2
3
4
// (x: type,...输入类型) => 输出类型 ,不要和箭头函数混淆了
let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
return x + y;
};

用接口定义函数的形状:

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

参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 后面 = 赋值默认参数,同 es6语法
// 用 `?` 表示可选的参数:,可选参数 必须接在 必需参数 后面
function buildName(firstName: string = 'unkownName', lastName?: string) {
if (lastName) {
return firstName + ' ' + lastName;
} else {
return firstName;
}
}
let tomcat = buildName('Jason', 'Wu');
let tom = buildName('Jason');

// 剩余参数,语法同es6
function push(array: any[], ...items: any[]) {
items.forEach(function(item) {
array.push(item);
});
}

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

重载

用重载定义多个 reverse 的函数类型:

1
2
3
4
5
6
7
8
9
10
11
12
// 精确表达,输入为数字的时候,输出也应该为数字
// 优先把精确的定义写在前面
function reverse(x: number): number;
// 输入为字符串的时候,输出也应该为字符串
function reverse(x: string): string;
function reverse(x: number | string): number | string {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else if (typeof x === 'string') {
return x.split('').reverse().join('');
}
}

类型断言

当使用 type1 | type2 这种情况,类型还不确定的时候,TS只会有其共有方法,获取单独属性方法就会报错, 手动断言指定一个值的类型。

需要断言的变量前加上即可

1
2
3
4
5
6
7
8
9
10
function getLength(something: string | number): number {
// if ((something).length) 报错,只有其共有方法
// 将 `something` 断言成 `string`
// 联合类型中不存在的类型不允许断言 eg:<boolean>something
if ((<string>something).length) {
return (<string>something).length;
} else {
return something.toString().length;
}
}

声明文件

声明语句:定义了全局变量的类型,仅仅会用于编译时的检查,在编译结果中会被删除。

把声明语句放到一个单独的文件(file.d.ts)中,以 .d.ts 结尾的文件就是声明文件。

当一个第三方库没有提供声明文件时,我们就需要自己书写声明文件了。

在不同的场景下,声明文件的内容和使用方式会有所区别。

全局变量

declare var

用来定义全局变量的类型。

1
2
3
// src/jQuery.d.ts
// 注入全局变量jQuery ,declare 只能用来声明类型,不能用来赋值
declare let jQuery: (selector: string) => any;
1
2
3
4
5
6
// src/index.ts
jQuery('#foo');
// 使用 declare let 定义的 jQuery 类型,允许修改这个全局变量
jQuery = function(selector) {
return document.querySelector(selector);
};

declare function

用来定义全局函数的类型。

1
2
3
4
// src/jQuery.d.ts
declare function jQuery(selector: string): any;
// 可以函数重载
declare function jQuery(domReadyCallback: () => any): any;
1
2
3
4
5
// src/index.ts
jQuery('#foo');
jQuery(function() {
alert('Dom Ready!');
});

declare class

定义全局 类

1
2
3
4
5
6
7
8
9
10
// src/Animal.d.ts
declare class Person {
name: string;
constructor(name: string);
sayHi(): string;
// declare里 只能用来定义类型,不能用来具体实现
// sayHi() { //报错
// return `My name is ${this.name}`;
// };
}
1
2
// src/index.ts
let cat = new Animal('Jason');

declare enum

使用 declare enum 定义的枚举类型

1
2
3
4
5
6
7
// src/Directions.d.ts
declare enum Directions {
Up,
Down,
Left,
Right
}
1
2
// src/index.ts
let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];

declare namespace

用来表示全局变量是一个对象,包含很多子属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/jQuery.d.ts
declare namespace jQuery {
// namespace内部可以直接使用function xxx,不必declare function xxx
function ajax(url: string, settings?: any): void;
const version: number;
class Event {
blur(eventType: EventType): void
}
enum EventType {
CustomClick
}
// 嵌套命名空间,声明深层对象属性, 但是仅有一个深层属性就可以不写namespace
namespace fn {
function extend(object: any): void;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
// src/index.ts
jQuery.ajax('/api/get_something');
console.log(jQuery.version);
const e = new jQuery.Event();
e.blur(jQuery.EventType.CustomClick);
jQuery.fn.extend({
check: function() {
return this.each(function() {
this.checked = true;
});
}
});

interface

在类型声明文件中,可以直接使用 interface 来声明一个全局的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/jQuery.d.ts
interface AjaxSettings {
method?: 'GET' | 'POST'
data?: any;
}
declare namespace jQuery {
function ajax(url: string, settings?: AjaxSettings): void;
// 接口和类型,最好还是放在namespace下,减少全局变量
interface AjaxSettings {
method?: 'GET' | 'POST'
data?: any;
}
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/index.ts
let settings: AjaxSettings = {
method: 'POST',
data: {
name: 'foo'
}
};
jQuery.ajax('/api/post_something', settings);
// 在命名空间里声明的接口 加上前缀 namespaceName.interfaceName...
let settings: jQuery.AjaxSettings = {
method: 'POST',
data: {
name: 'foo'
}
};

声明合并

可以组合多个声明语句,它们不冲突

1
2
3
4
5
// src/jQuery.d.ts
declare function jQuery(selector: string): any;
declare namespace jQuery {
function ajax(url: string, settings?: any): void;
}
1
2
// src/index.ts
jQuery('#foo');jQuery.ajax('/api/get_something');

npm包

判断声明文件是否已经存在。

  1. package.json中有types字段,或有一个index.d.ts声明文件(推荐)。
  2. 发布到@types里,安装试一下有无 cnpm i @types/foo --save-dev作者没有发布,一般由其他人补充。

如果以上两种方法都没有,就只能自己创建

创建一个 types 目录,专门用来管理自己写的声明文件,将 foo 的声明文件放到 types/foo/index.d.ts 中。这种方式需要配置下 tsconfig.json 中的 pathsbaseUrl 字段。

目录结构:

1
2
3
4
5
6
7
/path/to/project
├── src
| └── index.ts
├── types
| └── foo
| └── index.d.ts
└── tsconfig.json

tsconfig.json 内容:

1
2
3
4
5
6
7
8
9
{
"compilerOptions": {
"module": "commonjs",
"baseUrl": "./",
"paths": {
"*": ["types/*"]
}
}
}

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

export

只有在声明文件中使用 export 导出,然后在使用方 import 导入后,才会应用到这些类型声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 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;
}
// 导出一个拥有子属性的对象
export namespace foo {
const name: string;
namespace bar {
function baz(): string;
}
}

// 也可以使用 declare声明,再一并导出
declare const name: string;
declare function getName(): string;
declare class Animal {
constructor(name: string);
sayHi(): string;
}
declare enum Directions {
Up,
Down,
Left,
Right
}
// 接口不需要 declare声明
interface Options {
data: any;
}

export { name, getName, Animal, Directions, Options };

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 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'
}
};

import { foo } from 'foo';

console.log(foo.name);
foo.bar.baz();

UMD 库

既可以通过 <script>标签引入,又可以通过 import 导入的库,称为 UMD 库。

1
2
3
4
5
6
7
8
9
10
// types/foo/index.d.ts
// 使用 export as namespace xxx
export as namespace foo;
export default foo; // ES6 标准
// export = foo; ts 为了兼容 AMD 规范和 commonjs 规范而创立的新语法

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

直接扩展全局变量

扩展 String 类型

1
2
3
4
5
// 给 `String` 添加属性或方法。
interface String {
prependHello(): string;
}
'foo'.prependHello();

也可以使用 declare namespace 给已有的命名空间添加类型声明

1
2
3
4
5
6
7
8
9
// types/jquery-plugin/index.d.ts
declare namespace JQuery {
interface CustomOptions {
bar: string;
}
}
interface JQueryStatic {
foo(options: JQuery.CustomOptions): string;
}
1
2
3
4
// src/index.ts
jQuery.foo({
bar: ''
});

在 npm 包或 UMD 库中扩展全局变量

对于一个 npm 包或者 UMD 库的声明文件,只有 export 导出的类型声明才能被导入。如果导入npm 包或 UMD 库之后要扩展全局变量,则需要使用declare global在声明文件中扩展全局变量的类型。

declare global

1
2
3
4
5
6
7
8
// types/foo/index.d.ts
declare global {
interface String {
prependHello(): string;
}
}
// 告诉编译器这是一个 模块 的声明文件
export {};
1
2
// src/index.ts
'bar'.prependHello();

模块插件

原有模块已经有了类型声明文件,而插件模块没有类型声明文件,就会导致类型不完整,缺少插件部分的类型。ts 提供了一个语法 declare module,它可以用来扩展原有模块的类型。

declare module

需要在类型声明文件中先引用原有模块,再使用 declare module 扩展原有模块

1
2
3
4
5
6
7
8
9
10
11
// types/moment-plugin/index.d.ts
import * as moment from 'moment';
declare module 'moment' {
export function foo(): moment.CalendarKey;
}
//在一个文件中 可声明多个模块的类型
declare module 'bar' {
export interface Bar {
bar: string;
}
}
1
2
3
4
5
6
7
// src/index.ts
import * as moment from 'moment';
import 'moment-plugin';
moment.foo();

import { Bar } from 'bar';
let B: Bar;

三斜线指令

应用场景:

  • 当我们在书写一个全局变量的声明文件时

在全局变量的声明文件中,一旦出现了 import, export 关键字。那么他就会被视为一个 npm 包或 UMD 库。写一个全局变量的声明文件时,如果需要引用另一个库的类型,那么就必须用三斜线指令了。

1
2
3
4
5
// types/jquery-plugin/index.d.ts
// `///` 后面使用 xml 的格式添加了对 `jquery` 类型的依赖,就可以在声明文件中使用 `JQuery.AjaxSettings` 类型了.
// 三斜线指令必须放在文件的最顶端,除了注释
/// <reference types="jquery" />
declare function foo(options: JQuery.AjaxSettings): string;
1
2
// src/index.ts
foo({});
  • 当我们需要依赖一个全局变量的声明文件时

由于全局变量不支持通过 import 导入,必须使用三斜线指令来引入

1
2
3
4
5
// types/node-plugin/index.d.ts
// 由于引入的 `node` 中的类型都是全局变量的类型,通过三斜线指引入了 `node` 的类型
/// <reference types="node" />
// 声明文件中使用了 `NodeJS.Process` 这个类型
export function foo(p: NodeJS.Process): string;
1
2
3
4
// src/index.ts
import { foo } from 'node-plugin';
// 使用到 `foo` 的时候,传入了 `node` 中的全局变量 `process`
foo(global.process);

拆分声明文件

当我们的全局变量的声明文件太大时,可以通过拆分为多个文件,然后在一个入口文件中将它们一一引入,来提高代码的可维护性。

比如 jQuery 的声明文件就是这样的:

1
2
3
4
5
6
7
8
9
10
// node_modules/@types/jquery/index.d.ts
// types 用于声明对另一个 '库' 的依赖
/// <reference types="sizzle" />
// path 用于声明对另一个 '文件' 的依赖
/// <reference path="JQueryStatic.d.ts" />
/// <reference path="JQuery.d.ts" />
/// <reference path="misc.d.ts" />
/// <reference path="legacy.d.ts" />

export = jQuery;

自动生成声明文件

如果库的源码本身就是由 ts 写的,那么在使用 tsc 脚本将 ts 编译为 js 的时候,命令行添加 --declaration(-d) 选项,就可以同时也生成 .d.ts 声明文件了。

或者在 tsconfig.json 中添加 declaration: true 选项。

1
2
3
4
5
6
7
8
9
{
"compilerOptions": {
"module": "commonjs",
// `outDir` 选项,将 ts 文件的编译结果输出到 `lib` 目录下
"outDir": "lib",
// 将会由 ts 文件自动生成 `.d.ts` 声明文件
"declaration": true,
}
}

发布声明文件

  1. 将声明文件和源码放在一起 (自己写的库)

    需要满足以下条件之一,才能被正确的识别:

    • package.json 中的 typestypings 字段指定一个类型声明文件地址。
    1
    2
    3
    4
    5
    6
    7
    {
    "name": "foo",
    "version": "1.0.0",
    // 入口文件
    "main": "lib/index.js",
    "types": "foo.d.ts",...
    }
    • 如果没有指定 typestypings,那么就会在根目录下寻找 index.d.ts 文件
    • 如果根目录下没有找到 index.d.ts 文件,那么就会寻找入口文件,是否存在对应同名不同后缀的 .d.ts 文件
  2. 将声明文件发布到 @types 下(原作者不愿意合并 pull request 时,补充别人的仓库)

    • DefinitelyTyped 创建一个 pull-request,其中包含了类型声明文件,测试代码,以及 tsconfig.json 等。通过测试,稍后就会被自动发布到 @types 下。
    • DefinitelyTyped 文档

内置对象

ECMAScript 的内置对象

BooleanErrorDateRegExp 等。

可以在 TypeScript 中将变量定义为这些类型:

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

DOM 和 BOM 的内置对象

DocumentHTMLElementEventNodeList 等。

TypeScript 中会经常用到这些类型:

1
2
3
4
5
let body: HTMLElement = document.body;
let allDiv: NodeList = document.querySelectorAll('div');
document.addEventListener('click', function(e: MouseEvent) {
// Do something
});

用 TypeScript 写 Node.js

TypeScript 核心库的定义中不包含 Node.js 部分,需要引入第三方声明文件:

1
npm install @types/node --save-dev

总结:

基础部分学完了,总的来说TS可以规范变量,对象,函数类型。声明文件那部分学的不太明白,可能还做过大型库,项目,水平没上来,再多看一些文章加深理解吧。