TypeScript从平凡到不凡(基础篇)

  由于JS语言本身的局限,难以胜任和维护大型的项目,因此微软在2013年发布了正式版本的TypeScript,使其能够胜任大型项目的开发维护;现在,很多流行的框架和类库都已经转向采用TypeScript进行开发,那么TS相比与JS的优势在哪里?我们就来看一下TS是凭借什么从而实现逆袭的。

从平凡到不凡,英文原句:From Zero To Hero,让我们学习TS从Zero开始,到达Hero

TypeScript简介

  那么,什么是TypeScript呢?

TypeScript是JavaScript类型的超集,它可以编译成纯JavaScript。

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  这里的超集是数学上的概念,与它相对的概念就是子集;所谓的超集子集,他们是成对出现的,有超集必然有子集;TypeScript是JavaScript类型的超集,也就是说JavaScript具有的特性和功能,TypeScript全都有,并在这个基础上有一些JavaScript不具备的特性和功能,形成了自己的优势。

  用图形来表示就是这样的:

TypeScript是超集

  那么问题来了,TypeScript的优势体现在哪里呢?我们都知道JavaScript是弱类型语言,它没有Java一样对变量类型严苛的约束,这样带来的灵活性一方面能够让自身降低准入门槛,蓬勃发展,一直稳居GitHub热门编程语言宝座;另一方面,灵活性也使得它的代码质量参差不齐,维护成本高以及容易产生运行时错误。

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  从TypeScript名字中的类型就能看出,其核心特性就是它的类型系统,来弥补JavaScript灵活性带来的弊端;因此TypeScript相较于JavaScript有以下优势:

  • 类型系统增强了代码的可读性和可维护性;
  • 在编译阶段就能发现程序的错误;
  • 增强了编辑器和IDE的功能,包括代码补全、接口提示、跳转到定义、重构等;
  • 兼容性强,现有js代码可与TypeScript一起工作,无需修改;

  好了,夸了这么多彩虹屁,我们还是来到安装环节:

夸赞

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

1
npm install -g typescript

  全局安装后,我们就可以在任何地方通过tsc命令编译我们的TypeScript文件了,比如我们常见的hello.ts:

1
2
3
4
5
function hello(person: string) {
return "Hello, " + person;
}
var user = "Jane User";
hello(user);

  然后执行编译命令,就变成我们常见的js了:

1
tsc hello.ts

  在上面ts文件中,使用:来指定变量的类型。

TypeScript基础

  我们先从TypeScript的一些基本概念开始介绍,让大家对它有一个基本的了解(以下简称ts)。

基本数据类型

布尔值

  布尔值是基础的数据类型,在ts中,使用boolean定义布尔值类型:

1
let isDone: boolean = false;

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  我们需要区分一下booleanBoolean,前者是用来定义类型,后者是一个构造函数,用来创建对象:

1
2
let isDone: object = new Boolean(1);
let isDoneToo: boolean = Boolean(1);

  这里new Boolean()返回的是一个Boolean对象,本质上是对象,而Boolean()直接调用也可以返回一个boolean类型的值;这里要注意两者的区别,下面的几种基本数据类型也都有这样的区别,不在赘述。

数值

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

  ts用number表示数值类型,除了十进制和十六进制的数值,还支持二进制和八进制,后面两者会被编译成二进制的数字。

字符串

1
2
3
let firstName: string = 'Jack'
let lastName: string = "Bob"
let name: string = `${firstName} ${lastName}`

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  我们使用string表示文本字符串类型,可以使用单引号(’)、双引号(”)或者es6中的模板字符串(`)。

Void

  在ts中,可以用void来表示没有任何返回值的函数:

1
2
3
function helloTS(): void{
console.log("i have no return value")
}

  声明一个void类型的变量没什么用,我们只能给它赋值undefined或者null:

1
2
3
let a: void = undefined
//报错:不能将类型“1”分配给类型“void”。
a = 1

Never

  never类型表示那些永远不会存在值的类型;例如, never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。

  此外,变量也可能是never类型,当它们被永不为真的类型保护所约束时。为了让大家更好的理解never类型,我们来举一些实际的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 定义never类型的变量
let foo: never;

// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
throw new Error(message);
}

// 推断的返回值类型为never
function fail() {
return error("Something failed");
}

// 返回never的函数必须存在无法达到的终点
function infiniteLoop(): never {
while (true) {
}
}

  那么never类型有什么用呢?尤大大在知乎上举了一个例子,利用never的特性来实现详细的检查:

1
2
3
4
5
6
7
8
9
10
11
12
type Foo = string | number;

function handleValue(foo: Foo) {
if (typeof foo === "string") {
// 这里 foo 被收窄为 string 类型
} else if (typeof foo === "number") {
// 这里 foo 被收窄为 number 类型
} else {
// 永远到达不了
const check: never = foo;
}
}

  假设我们定义了一个联合类型,在函数中进行类型判断;如果逻辑正确,那么最后的else是永远到达不了的;但是如果有一天你的同事修改了Foo的类型:

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

1
type Foo = string | number | boolean;

  同时忘记修改handleValue函数中的控制流程,那么这时,else中的check变量就会被收窄为boolean类型,就会产生编译错误。

  通过这样的方式,我们可以穷尽Foo所有可能的类型,避免新增了联合类型却没有对应的实现。

Unknown

  当我们在写应用的时候可能会需要描述一个我们还不知道其类型的变量;这些值可以来自动态的内容,例如从用户获得,或者我们想在API中接收所有可能类型的值。在这些情况下,我们想要让编译器以及未来的用户知道这个变量可以是任意类型。这个时候我们会使用 unknown 类型。

1
2
3
4
5
6
let maybe: unknown;

maybe = 1;
maybe = "2";
maybe = {};
maybe = [];

  如果你有一个 unknwon 类型的变量,你可以通过进行 typeof比较或者更高级的类型检查来将其的类型范围缩小:

1
2
3
4
5
6
7
8
9
let maybe: unknown;

if (typeof maybe === "string") {
maybe.substr(0, 2);
} else if (typeof maybe === "number") {
maybe.toString();
} else if (typeof maybe === "boolean") {
maybe.valueOf();
}

Null和Undefined

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

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

  这两个数据类型只能分别唯一定义各自的数据,因此本身用处不是很大;不过undefined和null是所有类型的子类型,因此它们可以赋值给其他类型的变量,包括void:

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

1
let num = undefined

Any

  在编程的时候,我们可能还没有确定一个变量的数据类型,这个值可能来自动态的内容,比如接口数据,或者第三方的库,我们不希望对它进行类型检查,因此可以用任意值any来标记允许变量赋值为任意数据类型。

1
2
3
let someValue:any = 10
someValue = '20'
someValue = {}

  在任意值上可以访问任何属性和调用任何函数

1
2
3
4
5
6
let someValue: any = ''
console.log(someValue.name)
console.log(someValue.name.firstName)

someValue.showName()
someValue.name.showName()

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

1
2
3
4
5
6
7
let sth
// 等价于
// let sth:any
sth = "seven"
sth = 7

sth.setName("Tom")

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

Never和Void的区别

  我们看到上面的Never和Void有点类似,都可以用于描述函数没有返回值,但是两者有本质的区别:

  • 没有显式返回值的函数会隐式返回 undefined 。尽管我们通常说这样的函数什么也不返回,但实际上它是会返回的。在这些情况下,我们通常忽略返回值。在ts中这些函数的返回类型被推断为 void
  • 具有never返回类型的函数永不返回,它也不返回 undefined。该函数没有正常完成,这意味着它可能会抛出异常或根本无法退出执行。
1
2
3
4
5
6
7
8
// 正常运行
function returnVoid(): void {
return undefined;
}
// 报错:
// A function returning 'never' cannot have a reachable end point.
function returnNever(): never {
}

Unknow和Any的区别

  在ts中,当我们不确定一个类型是什么类型的,可以选择给其声明为any或者unkown。但实际上,ts推荐使用unknown,因为unknown是类型安全的。

  如果是any类型,你可以对它任意的取值和赋值,完全放弃了类型检查;但unknow类型就不一样了,必须进行类型收窄才能进行取值。

1
2
3
4
5
6
7
8
9
10
11
12
13
let maybe1: any;
let maybe2: unknown

maybe1 = 1
maybe1 = {}
//正常运行
maybe1.children.origin.hello()

maybe2 = 2
maybe2 = {}
// 报错:
// //Property 'length' does not exist on type 'unknown'
console.log(maybe2.length)

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  这反映了两者的一个本质区别:

  • any是任意类型的父类型,同时也任意类型的子类型
  • unknown是任意类型的父类型,但仅此而已。

类型推论

  在ts中,如果一个变量没有明确指定类型,那么会按照类型推论的规则来推断出一个类型。

1
2
3
4
let myNumber = 7
myNumber = '8'
// 报错:
// Type 'string' is not assignable to type 'number'.

  这里变量myNumber被推断为数字,因此再次改变其类型就报错了。

联合类型

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

1
2
3
let myData: string | number
myData = 7
myData = "8"

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  我们用竖线分隔每个类型,允许myData的类型可以是string或者number,但不允许是其他类型。

  偶尔我们会遇到函数传参时,一个参数允许传入不同类型,也可以用到联合类型:

1
2
3
4
5
function say(age: string | number){
console.log(age)
}
say(10)
say("11")

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

1
2
3
4
5
6
function say(age: string | number){
// toString是共有的方法
console.log(age.toString())
// 报错:Property 'length' does not exist on type 'number'.
console.log(age.length)
}

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

1
2
3
4
5
6
7
8
let myData: string | number

myData = "8"
console.log(myData.length)

myData = 7
// 报错:Property 'length' does not exist on type 'number'.
console.log(myData.length)

数组的类型

  在ts中我们可以用多种方式来定义数组。

类型加[]

  最简单的表示数组是使用类型+[]的形式:

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

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  数组的项中不能出现其他的类型,包括调用数组添加的方法:

1
2
3
4
5
6
// 报错:Type 'string' is not assignable to type 'number'.
let list: number[] = [1, 2, '3']

let list1: number[] = []
// 报错:Argument of type 'string' is not assignable to parameter of type 'number'
list1.push('3')

  针对一些复杂结构的数组,我们可以通过any来表示数组中允许出现任意的类型

1
let list: any[] = ['1', 2, { name: 'Lucy', age: 18 }]

数组泛型

  我们可以使用数组泛型来表示数组:

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

  更多关于泛型的下面会涉及。

接口表示

  我们也可以用接口来表示一个数组:

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

1
2
3
4
5
interface NumberArray {
[index: number]: number;
}

let list1: NumberArray = [1, 2, 3];

  这样定义数组比较繁琐,不常用,但是我们会用这种形式来表示类数组:

1
2
3
4
5
function sum() {
// 报错:
//Type 'IArguments' is missing the following properties from type 'any[]': pop, push, concat, join, and 15 more.
let arg: any[] = arguments;
}

  我们直接用数组的类型来表达arguments会报错,因为它本身不是一个数组,没有数组的push、pop等函数,我们可以通过接口的方式:

1
2
3
4
5
6
7
8
9
interface ArgumentInterface {
[index: number]: any;
length: number;
callee: Function;
}

function sum() {
let arg: ArgumentInterface = arguments;
}

  这里的ArgumentInterface实际上在ts内部已经定义好了,我们可以直接拿来用:

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

枚举

  枚举在项目中随处可见,比如一系列月份、日期的选择,或者和后台约定的列表范围内的选择等;通过ts,我们可以更清晰更方便的来定义枚举值。

1
2
3
4
5
6
7
8
9
enum Days {
Sun,
Mon,
Tue,
Wed,
Thu,
Fri,
Sat,
}

  枚举通过enum来定义,枚举成员会被从0开始递增赋值,同时也会进行枚举值到枚举名的反向映射

1
2
3
4
5
6
7
console.log(Days['Sun'] === 0)
console.log(Days['Mon'] === 1)
console.log(Days['Tue'] === 2)

console.log(Days[0] === 'Sun')
console.log(Days[1] === 'Mon')
console.log(Days[2] === 'Tue')

  事实上,上面枚举代码会被编译为以下js代码:

1
2
3
4
5
6
7
8
9
10
var Days;
(function (Days) {
Days[Days["Sun"] = 0] = "Sun";
Days[Days["Mon"] = 1] = "Mon";
Days[Days["Tue"] = 2] = "Tue";
Days[Days["Wed"] = 3] = "Wed";
Days[Days["Thu"] = 4] = "Thu";
Days[Days["Fri"] = 5] = "Fri";
Days[Days["Sat"] = 6] = "Sat";
})(Days || (Days = {}));

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  如果我们对枚举值有其他需求,可以进行手动赋值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum Days {
Sun = 7,
Mon = 1,
Tue,
Wed,
Thu,
Fri,
Sat,
}

console.log(Days['Sun'] === 7)
console.log(Days['Mon'] === 1)
console.log(Days['Tue'] === 2)
console.log(Days['Wed'] === 4)

  未手动赋值的枚举项会接着上一个枚举项的值递增;如果手动赋值的枚举项和后面递增的重复了,ts也不会报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
enum Days {
Sun = 3,
Mon = 1,
Tue,
Wed,
Thu,
Fri,
Sat,
}
console.log(Days["Sun"] === 3); // true
console.log(Days["Wed"] === 3); // true
console.log(Days[3] === "Sun"); // false
console.log(Days[3] === "Wed"); // true

  可以看到,当递增到3时,Wed与前面的Sun重复了,枚举项的值还是能继续取到;但是枚举值对应的枚举项会被覆盖,上面代码会被如下编译:

1
2
3
4
5
6
7
8
9
10
var Days;
(function (Days) {
Days[Days["Sun"] = 3] = "Sun";
Days[Days["Mon"] = 1] = "Mon";
Days[Days["Tue"] = 2] = "Tue";
Days[Days["Wed"] = 3] = "Wed";
Days[Days["Thu"] = 4] = "Thu";
Days[Days["Fri"] = 5] = "Fri";
Days[Days["Sat"] = 6] = "Sat";
})(Days || (Days = {}));

  手动赋值的枚举值也可以为小数或者负数,后续为赋值的项递增步长仍为1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum Days {
Sun = -2.5,
Mon,
Tue,
Wed = 3.5,
Thu,
Fri,
Sat,
}

console.log(Days['Mon'] === -1.5)
console.log(Days['Tue'] === -0.5)
console.log(Days['Thu'] === 4.5)
console.log(Days['Fri'] === 5.5)

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  虽然最后编译成对象,但是枚举项的值是不可修改的:

1
2
3
4
5
6
7
8
enum Days {
Sun,
Mon,
Tue,
}
// 报错
// Cannot assign to 'Sun' because it is a read-only property.
Days.Sun = 7

字符串枚举

  枚举值还可以设为字符串:

1
2
3
4
5
6
enum Days {
Sun = '0',
Mon = '1',
Tue = '2',
Wed = '3',
}

  由于未设置的枚举值是递增关系,因此我们我们不能将中间的枚举值设为字符串,这样它后面的枚举值就不知道从哪里开始了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 报错:
// Enum member must have initializer.
enum Days {
Sun,
Mon,
Tue = '2',
Wed = '3',
Thu,
Fri,
Sat,
}
//这样是可以的
enum Days1 {
Sun,
Mon,
Tue,
Wed,
Thu,
Fri,
Sat = '6',
}

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  字符串的枚举值是不做双向映射的:

1
2
3
4
5
6
enum Days {
Sun = '0',
Mon = '1',
Tue = '2',
Wed = '3',
}

  上面代码会被如下编译:

1
2
3
4
5
6
7
var Days1;
(function (Days1) {
Days1["Sun"] = "0";
Days1["Mon"] = "1";
Days1["Tue"] = "2";
Days1["Wed"] = "3";
})(Days1 || (Days1 = {}));

常数项和计算项

  枚举项可以分为常数项计算项,常数项有以下三种情况:

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  1. 它是枚举的第一个成员且没有初始化器
  2. 它不带有初始化器且它之前的枚举成员是一个数字常量
  3. 枚举成员使用常量枚举表达式初始化

  其他的情况都是计算项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum befor {
num,
}

enum test {
// 常数项
First,
Second = befor.num,
Add = 1 + 2,
NOR = 0 | 3,
OR = 1 & 2,
// 计算项
LEN = "123".length,
RAM = Math.random()
}

  常数项会在编译时计算出结果,然后以常量的形式出现在代码中,上述代码会被如下编译:

1
2
3
4
5
6
7
8
9
10
var test;
(function (test) {
test[test["First"] = 0] = "First";
test[test["Second"] = 0] = "Second";
test[test["Add"] = 3] = "Add";
test[test["NOR"] = 3] = "NOR";
test[test["OR"] = 0] = "OR";
test[test["LEN"] = "123".length] = "LEN";
test[test["RAM"] = Math.random()] = "RAM";
})(test || (test = {}));

常量枚举

  常量枚举是通过const enum定义的枚举类型:

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

1
2
3
4
5
const enum Month {
Jan,
Feb,
Mar,
}

  常量枚举在编译阶段会被移除;当我们不需要一个对象,而只需要对象的值,就可以使用常量枚举,这样就能避免在编译时生成多余的代码和间接引用:

1
2
3
4
5
6
7
8
9
10
const enum Month {
Jan,
Feb,
Mar,
}
//报错:
//'const' enums can only be used in property or index access expressions or the right hand side of an import declaration or export assignment or type query.
console.log(Month)
//正常运行,输出0
console.log(Month.Jan);

  由于常量枚举在编译时会被移除,因此常量枚举不能包含计算项:

1
2
3
4
5
6
7
8
const enum Month {
Jan,
//正常运行
Feb = 1 + 2,
// 报错:
// const enum member initializers can only contain literal values and other computed enum values.
Mar = "123".length,
}

外部枚举

  外部枚举是使用declare enum定义的枚举类型:

1
2
3
4
5
6
declare enum Directions {
Up,
Down,
Left,
Right
}

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  外部枚举与声明语句一样,常出现在声明文件中。

对象的类型-接口

  在ts中,我们使用接口来定义对象的类型;有时这也被叫做鸭式辨型法

像鸭子一样走路、游泳和嘎嘎叫的就是鸭子

  也就是说,哪怕是一条狗,如果它也能像鸭子那样走路、游泳和叫,那么我们也认为它是一只鸭子。

鸭子

  很多童鞋可能就难以理解,就算事实上真的有一条狗这么去走路这么去叫,它本质上也是狗,怎么会变成鸭子呢?这不是典型的指鹿为马么?

  但是如果我们把这两类动物放到程序中来,

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
function Duck(name){
this.name = name;
this.duckWalk = function(){
console.log("我是一只鸭子,我快乐地走着")
}
}
function Dog(name){
this.name = name;
this.duckWalk = function(){
console.log("我是一只鸭子,我快乐地走着")
}
this.bark = function(){
console.log("我是一只狗子,我快乐地叫着")
}
}
var duck = new Duck("a")
var dog = new Dog("b")

// 驱动鸭子们往前走
function needDuckWalk(duck){
duck.duckWalk()
}

needDuckWalk(duck)
needDuckWalk(dog)

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  这里构造了duck和dog这两个动物实例,我们需要驱动鸭子往前走起来(即调用他们的函数),但是程序并不需要确切的区分谁是谁,只要能够保证它有duckWalk函数就可以了,这就是所谓的鸭式辨型法。

确定属性

  在面向对象的语言中,接口就是来保证对象有我们需要的函数,它是对类的行为进行的抽象。

1
2
3
4
5
6
7
8
9
10
11
12
13
interface Duck {
name: string;
age: number;
gender: boolean;
walk: Function;
}

let dog: Duck = {
name: "yellow",
age: 2,
gender: true,
walk() {},
};

  我们定义了一个接口Duck,规定了字面量dog的类型是Duck,这样就约束了dog的属性必须和接口定义的保持一致,如果多一些或者少一些属性都是不允许的,都会报错。

1
2
3
4
5
6
// 报错:
// Property 'gender' is missing in type '{ name: string; age: number; }' but required in type 'Duck'.
let dog: Duck = {
name: "yellow",
age: 2,
};

可选属性

  但是有一些属性是可有可无的,我们不希望完全匹配,那么就可以用可选属性

1
2
3
4
5
6
7
8
9
10
interface Duck {
name: string;
age?: number;
birth?: string;
gender?: boolean;
}

let dog: Duck = {
name: "yellow",
};

  可选属性在属性后面加一个问号,表示该属性是非必须的,一个接口中可以同时存在多个可选属性;但是这时其他属性还是不允许添加的。

任意属性

  我们希望在一个接口中添加任意的属性,可以通过任意属性的方式:

1
2
3
4
5
6
7
8
9
10
11
interface Duck {
name: string;
age?: number;
[propName: string]: any;
}

let dog: Duck = {
name: "yellow",
birth: "",
color: ["red"]
};

  不过需要注意的是,如果我们定义了任意属性,那么我们在上面定义的确定属性和可选属性必须是它的类型的子集:

1
2
3
4
5
6
7
8
9
// 报错:
// Property 'age' of type 'number' is not assignable to 'string' index type 'string'.
// Property 'gender' of type 'boolean' is not assignable to 'string' index type 'string'.
interface Duck {
name: string;
age?: number;
gender?: boolean;
[propName: string]: string;
}

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  这里age和gender分别是number和boolean类型,不是string类型的子集,因此就会报错;在一个接口中只能定义一个任意类型,如果接口中有多个类型的属性,可以使用联合类型:

1
2
3
4
5
6
7
8
9
10
interface Duck {
name: string;
age?: number;
[propName: string]: string | number;
}

let dog: Duck = {
name: "yellow",
birth: ""
};

只读属性

  有时候我们希望有一些属性只能被读取,不能进行修改,可以将其定义为只读属性readonly,比如唯一标识的id信息;只读属性只能在初始化时被赋值:

1
2
3
4
5
6
7
8
9
10
11
12
interface Duck {
readonly id: number;
name: string;
}

let dog: Duck = {
id: 9527,
name: "yellow",
};
// 报错:
// Cannot assign to 'id' because it is a read-only property.
dog.id = 9528;

  有些童鞋可能想到了,那我初始化对象时不赋值,后面不就可以再修改只读属性的值了吗?

1
2
3
4
5
6
7
8
9
10
interface Duck {
readonly id: number;
name: string;
}

// 报错:
// Property 'id' is missing in type '{ name: string; }' but required in type 'Duck'.
let dog: Duck = {
name: "yellow",
};

  然而很遗憾,你就会收获另一个错误。

想不到吧

  只读属性的性质和确定属性一样,是不能缺少的;有些童鞋可能又想到了,那我把可选属性也加上不就行了,变成了只读可选属性:

1
2
3
4
5
6
7
8
9
10
11
interface Duck {
readonly id?: number;
name: string;
}

let dog: Duck = {
name: "yellow",
};
// 报错:
// Cannot assign to 'id' because it is a read-only property.
dog.id = 12;

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  这样也是不行的,因此我们发现了:

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

函数的类型

  在js中有两种声明函数的方式:函数声明和函数表达式,我们先来看下函数声明的类型定义,只需要把函数的输入和输出都考虑到即可:

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

  如果调用时的参数多于或者少于要求的参数,都会报错:

1
2
3
4
5
6
7
function sum(x: number, y: number) {
return x + y;
}
// An argument for 'y' was not provided.
sum(1)
// Expected 2 arguments, but got 3.
sum(1,2,3)

函数表达式

  我们也可以对函数表达式进行相同的类型定义:

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

  不过这样只对等号右侧的匿名函数进行了类型定义,而等号左侧的变量sum则是通过类型推论得到的,我们也可以手动给它添加类型:

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

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  注意这里的=>和es6中的=>是不一样的,ts中的=>用来表示函数的定义,左边是输入类型,右边是输出类型。

  我们也可以通过接口来定义函数表达式

1
2
3
4
5
6
interface ISumFunc {
(x: number, y: number): number;
}
let sum: ISumFunc = function (x, y) {
return x + y;
};

可选参数

  上面我们说到,对于函数参数个数,是必须按照定义传参的,那么如何定义可选参数呢?和接口定义对象的可选属性类似,我们也用到了?来表示:

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

1
2
3
4
5
6
7
8
9
function buildName(firstName: string, lastName?: string) {
if (lastName) {
return firstName + ' ' + lastName;
} else {
return firstName;
}
}
buildName("Lucy")
buildName("Lucy", "Jessica")

  需要注意的是,可选参数必须在必须参数后面

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

1
2
3
4
5
6
7
8
9
// 报错:
// A required parameter cannot follow an optional parameter.
function buildName(firstName?: string, lastName: string) {
if (lastName) {
return firstName + ' ' + lastName;
} else {
return firstName;
}
}

参数默认值

  和es6的函数一样,在ts中我们也可以给函数的参数添加默认值:

1
2
3
4
5
6
7
function buildName(firstName: string = "Tom", lastName: string = "Cat") {
if (lastName) {
return firstName + " " + lastName;
} else {
return firstName;
}
}

  ts会将添加了默认值的参数自动识别为可选参数,此时就不受可选参数必须在必须参数后面限制:

1
2
3
4
5
6
7
function buildName(firstName?: string, lastName: string = "Cat") {
if (lastName) {
return firstName + " " + lastName;
} else {
return firstName;
}
}

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

剩余参数

  在es6中,我们可以通过...rest的方式获取函数中剩余的参数:

1
2
3
4
5
function getRest(first, ...rest: any[]) {
rest.map((el) => {});
}

getRest(1, 2, 3, 4, 5);

  rest参数本质上是一个any[]类型的数组,它只能作为函数的最后一个参数。

函数重载

  函数重载是为同一个函数提供多种函数类型的定义来进行函数重载;我们可以定义多个函数重载的类型:

1
2
3
function add(x: number, y: string): void;
function add(x: string, y: number): void;
function add(x: string | number, y: number | string): void {}

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  需要注意的是,必须要把精确的定义放在前面,最后函数实现时,需要使用联合类型或者任意类型,把所有可能的输入类型全部包含进去,以具体实现;即最下面的方法需要兼容上面的方法。

  上面的例子中,虽然我们定义了3个重载的add函数,前两次都是函数的定义,最后一次是函数的实现,因此本质上我们只有定义了两次:

1
2
3
4
5
6
7
8
9
10
11
function add(x: number, y: string): void;
function add(x: string, y: number): void;
function add(x: string | number, y: number | string): void {}

//通过
add(1, '1')
//通过
add('2', 2)
//报错:
//The call would have succeeded against this implementation, but implementation signatures of overloads are not externally visible.
add('2', '2')

  最后一个函数调用时我们并没有定义两个都是字符串的参数,因此报错。


谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

本网所有内容文字和图片,版权均属谢小飞所有,任何媒体、网站或个人未经本网协议授权不得转载、链接、转贴或以其他方式复制发布/发表。如需转载请关注公众号【前端壹读】后回复【转载】。