亲手码出TypeScript最前沿的教程(基础篇)

3,821 阅读14分钟

编者荐语: 本文旨在帮助大家在闲暇时间掌握TS用法,故抽出时间,学习整理TypeScript教程,本文大部分内容来自阮一峰老师的网站,大家放心阅读。

以下内容来自于TypeScript 入门教程

什么是TypeScript

TypeScript 是 JavaScript 的一个超集,主要提供了类型系统和对 ES6 的支持,它由 Microsoft 开发,代码开源于 GitHub 上。 根据官网翻译成中文是

TypeScript 是 JavaScript 的类型的超集,它可以编译成纯 JavaScript。编译出来的 JavaScript 可以运行在任何浏览器上。TypeScript 编译工具可以运行在任何服务器和任何系统上。TypeScript 是开源的。

为什么选择TypeScript

TypeScript 官网列举了一些优势,不过阮一峰老师愿意自己总结一下:

TypeScript 增加了代码的可读性和可维护性

  • 类型系统实际上是最好的文档,大部分的函数看看类型的定义就可以知道如何使用了
  • 可以在编译阶段就发现大部分错误,这总比在运行时候出错好
  • 增强了编辑器和 IDE 的功能,包括代码补全、接口提示、跳转到定义、代码重构等

TypeScript 非常包容

  • TypeScript 是 JavaScript 的超集,.js 文件可以直接重命名为 .ts 即可
  • 即使不显式的定义类型,也能够自动做出类型推论
  • 即使 TypeScript 编译报错,也可以生成 JavaScript 文件
  • 兼容第三方库,即使第三方库不是用 TypeScript 写的,也可以编写单独的类型文件供 TypeScript 读取

TypeScript 拥有活跃的社区

  • 大部分第三方库都有提供给 TypeScript 的类型定义文件
  • Angular、Vue、VS Code、Ant Design 等等耳熟能详的项目都是使用 TypeScript 编写的
  • TypeScript 拥抱了 ES6 规范,支持 ESNext 草案中处于第三阶状态(Stage 3)的特性

安装TypeScript

TypeScript 的命令行工具安装方法如下:

npm install -g typescript

以上命令会在全局环境下安装 tsc 命令,安装完成之后,我们就可以在任何地方执行 tsc 命令了。
编译一个 TypeScript 文件很简单:

tsc hello.ts

查看 TypeScript 的版本信息:

tsc -v

我们约定使用 TypeScript 编写的文件以 .ts 为后缀,用 TypeScript 编写 React 时,以 .tsx 为后缀。

接下来让我们全面拥抱TypeScript伟大的语言吧~

原始数据类型

JavaScript 的类型分为两种: 原始数据类型对象类型
以下部分介绍前五中原始数组类型在TypeScipt中的应用。

布尔值

在 TypeScript 中,使用 boolean 定义布尔值类型:

let isDone: boolean = false;
// 编译通过
// 后面约定,未强调编译错误的代码片段,默认为编译通过

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

let createdByNewBoolean: boolean = new Boolean(1);
// Type 'Boolean' is not assignable to type 'boolean'.

事实上 new Boolean() 返回的是一个 Boolean 对象

let createdByNewBoolean: Boolean = new Boolean(1);

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

数值(同Number)

使用 number 定义数值类型:

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

编译结果:

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

其中 0b1010 和 0o744 是 ES6 中的 二进制八进制 表示法,它们会被编译为十进制数字。

字符串(同Number)

使用 string 定义字符串类型:

let myName: string = 'Tom';
let myAge: number = 25;

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

空值(void)

JavaScript 没有空值(Void)的概念,在 TypeScript 中,可以用 void 表示没有任何返回值的函数:

function alertName(): void {
  alert('My name is Tom');
}

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

let unusable: void = undefined;

以下的两点Null和Undefined尤为需要注意一下,因为在js面试中也常问到。

Null和Undefined

在 TypeScript 中,可以使用 null 和 undefined 来定义这两个原始数据类型:

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

void 的区别是, undefinednull 是所有类型的 子类型 。也就是说 undefined 类型的变量,可以赋值给 number 类型的变量:

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

//  这样也不会报错
let u:undefined;
let num:number = u;

void 类型的变量不能赋值给 number 类型的变量,会报错

let u: void;
let num: number = u;
// Type 'void' is not assignable to type 'number'.

任意值

任意值(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 = 'hello';
console.log(anyThing.myName);
console.log(anyThing.myName.firstName);

也允许调用任何方法:

let anyThing: any = 'Tom';
anyThing.setName('Jerry');
anyThing.setName('Jerry').sayHello();
anyThing.myName.setFirstName('Cat');

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

未声明类型的变量

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

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

something.setName('Tom');

等价于

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

something.setName('Tom');

任意值的弊端

任意值一定要慎用,因为在声明any之后。它的任何操作,返回的类型均是 any 对变量类型的检查变为毫无意义,同时这也就失去了TypeScripe强类型检查语言的意义了。

场景1

模块的引入可能是由第三方库引进,无法确认变量的类型时候,应该用 any(不确定性类型) - 允许赋值为任意类型

let notSure: any = 4
notSure = 'maybe it is a string'
notSure = true

notSure.myName
notSure.getName() // 以上不会报任何错

接下来讲一下TypeScript与JavaScript不同的概念

联合类型

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

简单的例子

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

// Type 'boolean' is not assignable to type 'string | number'.
// Type 'boolean' is not assignable to type 'number'.

联合类型使用 | 分隔每个类型。 这里的 let myFavoriteNumber: string | number 的含义是,允许 myFavoriteNumber 的类型是 string 或者 number ,但是不能是其他类型。

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

TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们<b>只能访问此联合类型的所有类型里共有的属性或方法:</b>
function getLength(something: string | number): number {
  return something.length;
}
function getLength(something: string | number): number {
  return something.length;
}

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

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

访问 string 和 number 的共有属性是没问题的:

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

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

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
console.log(myFavoriteNumber.length); // 5
myFavoriteNumber = 7;
console.log(myFavoriteNumber.length); // 编译时报错

// Property 'length' does not exist on type 'number'.

上例中,第二行的 myFavoriteNumber 被推断成了 string ,访问它的 length 属性不会报错。

而第四行的 myFavoriteNumber 被推断成了 number ,访问它的 length 属性时就报错了。

对象的类型————接口

在 TypeScript 中,我们使用接口 (Interfaces) 来定义对象的类型。

什么是接口

在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implement)。

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

简单的例子

这里注意接口里面定义的对象要用“;”表示,与JS不同
interface Person {
  name: string;
  age: number;
}

let tom:Person = {
  name: 'Tom',
  age: 25
}

上面的例子中,我们定义了一个接口 Person ,接着定义了一个变量 tom ,它的类型是 Person 。这样,我们就约束了 tom 的形状必须和接口 Person 一致。

接口一般大写,如 Person

定义的变量比接口少了一些属性是不允许的:

interface Person {
  name: string;
  age: number;
}

let tom: Person = {
  name: 'Tom'
};

// Type '{ name: string; }' is not assignable to type 'Person'.
// Property 'age' is missing in type '{ name: string; }'.

定义的变量比接口多一些属性也是不允许的:

interface Person {
  name: string;
  age: number;
}

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

// Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'.
// Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.

可选属性

有时我们希望不要完全匹配一个形状,那么可以用可选属性:

interface Person {
  name: string;
  age?: number;
}

let tom:Person = {
  name: 'Tom'
}

可选属性的含义就是该属性可以不存在。
这是仍然不允许添加未定义的属性

interface Person {
  name: string;
  age?: number;
}

let tom:Person = {
  name: 'Tom',
  age: 25,
  gender: 'male'
}

// Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'.
// Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.

任意属性

有时候我们希望一个接口允许有任意的属性,可以使用如下方式:

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

let tom: Person = {
  name: 'Tom',
  gender: 'male'
}

使用 [propName: string] 定义了任意属性取 string 类型的值。

需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集

错误写法:

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

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

上例中,任意属性的值允许是 string ,但是可选属性 age 的值却是 numbernumber 不是 string 的子属性,所以报错了。

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

正确写法:

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

let tom:Person = {
  name: 'Tom',
  age: 25,
  gender: 'male'
}

只读属性

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

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

let tom: Person = {
  id: 89757,
  name: 'Tom',
  gender: 'male'
};

tom.id = 9527;
// Cannot assign to 'id' because it is a constant or a read-only property.

上例中,使用 readonly 定义的属性 id 初始化后, 又被赋值 了,所以报错了。

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

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

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

tom.id = 89757;

// 两处报错

报错信息有两处:

  1. 在对 tom 进行赋值的时候,没有给 id 赋值。
  2. 在给 tom. id 赋值的时候,由于它是 只读属性 ,所以报错了。

数组的类型

在 TypeScript 中,数组类型有多种定义方式,比较灵活。

「类型 + 方括号」

表示法 --- 最简单的数组表示法

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

数组的项中不允许出现其他的类型:

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

// Type 'string' is not assignable to type 'number'.

数组的一些方法参数也会根据数组在定义时约定的类型进行限制:

let fibonacci: number[] = [1, 1, 2, 3, 5];
fibonacci.push('8');

// Argument of type '"8"' is not assignable to parameter of type 'number'.

上例中,push 方法只允许传入 number 类型的参数,但是却传了一个 "8" 类型的参数,所以报错了。这里 "8" 是一个字符串字面量类型,会在后续章节中详细介绍。

用接口表示数组

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

NumberArray 表示:只要索引的类型是数字时,那么值的类型必须是数字。

虽然接口也可以用来描述数组,但是我们一般不会这么做,因为这种方式比前两种方式复杂多了。

不过有一种情况例外,那就是它常用来表示类数组。

类数组 - arguments

function sum() {
  let args: number[] = arguments;
}

// Type 'IArguments' is missing the following properties from type 'number[]': pop, push, concat, join, and 24 more.

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

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

在这个例子中,我们除了约束当索引的类型是数字时,值的类型必须是数字之外,也约束了它还有 lengthcallee 两个属性。
事实上常用的类数组都有自己的接口定义,如 IArguments, NodeList, HTMLCollection 等:

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

any在数组中的应用

一个比较常见的做法是,用 any 表示数组中允许出现任意类型:

let list: any[] = ['abcd', 25, { website: 'http://xcatliu.com' }]

函数的类型

函数声明

在 JavaScript 中,有两种常见的定义函数的方式——函数声明和函数表达式:

// 函数声明
function sum(x,y) {
  return x + y;
}

// 函数表达式
let mySum = function(x, y) {
  return x + y;
}

函数声明的类型定义

限制了参数的类型 和 返回值的 类型

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

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

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

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

可选参数

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

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

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

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

//  A required parameter cannot follow an optional parameter.

参数默认值

TypeScript 会将添加了默认值的参数识别为可选参数: 不传的话,默认值为 Cat

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

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

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

剩余参数

items 是一个数组。所以我们可以用数组的类型来定义它:

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

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

函数表达式

通过对函数声明式学习的了解,我们可能会写成这样:

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

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

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

注意不要混淆了 TypeScript 中的 => 和 ES6 中的 =>。 在 TypeScript 的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型

用接口定义函数的形状(拓展)

我们也可以使用接口的方式来定义一个函数(参数)(输出值)需要符合的形状:

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

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

采用函数表达式|接口定义函数的方式时,对等号左侧进行类型限制,可以保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变

看完三件事❤

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

  1. 点赞,转发让更多的人也能看到介绍内容(收藏不点赞,皆是耍流氓!!)
  2. 关注公众号 “前端时光屋”,不定期分享原创知识。
  3. 同时可以期待后续文章ing

也可以来我的个人博客:

前端时光屋:www.javascriptlab.top/