TypeScript学习笔记

在学习了es6的常见常用的知识之后,工作需要好好学习一下TS。一些与es6相同的特性就不写了。

在开始之前,可以通过npm安装TypeScript

1
$ npm install -g typescript

之后通过

1
$ tsc greeter.ts

会输出一个greeter.js文件

TypeScript 的特点和意义

特点:

  • JavaScript 的超集,包含了 JavaScript 的特性,并扩展了 JavaScript 的语法

  • 增加了静态类型、类、模块、接口、泛型和类型注解

  • 可用于开发大型的应用。强类型语言,静态类型检查,在编写代码的时候就可以发现问题。并且在修改旧的业务的时候,也可以根据变量的类型和上下文关系来判断是否有其他影响。

意义:

我对 TS 的理解,如果真的使用 TS 的项目,更多的想要使用 TS 的类型检查,新特性以及代码提示。

适合场景:

​ 中大型项目,需要长期维护的项目,底层库或框架,前提是,项目主要依赖的类库对TS支持良好。

不适合场景:

​ 小项目, 生命周期短(可能几天内),不经常维护。加上可能配置webpack、 babel等,也要花费比较多的时间,所以并不是很合适。

TypeScript 的类型,有些是 JavaScript 自带的,有些是 TypeScript 额外定义的。请分别列出这两类类型。并且对 TypeScript 额外定义的类型进行简要说明。

TypeScript 的额外类型:

  • 元组:元组类型用来表示已知元素数量和类型的数组,各元素的类型不必相同,对应位置的类型需要相同。
  • 枚举:枚举类型用于定义数值集合。
  • void: 用于标识方法返回值的类型,表示该方法没有返回值。
  • never:never 是其它类型(包括 null 和 undefined)的子类型,代表从不会出现的值。
  • unknown :未知类型
  • any:任意类型,如果任何值都指定为 any ,ts 将失去意义

JavaScript自带类型:

- Number
- String
- Boolean
- Array
- undefined
- null
- Object

简述数组( Array )类型与元组( Tuple )类型的异同。

元组:可以看作是数组的拓展,它表示已知元素数量和类型的数组。确切地说,是已知数组中每一个位置上的元素的类型

  • 当我们为元组赋值时,每个位置的值的类型都要对应
  • 当我们访问元组元素时候,也会检查我们对元素的操作
  • 在 2.6 及之前版本中,超出规定个数的元素称作越界元素,但是只要越界元素的类型是定义的类型中的一种即可。在 2.6 之后的版本,去掉了这个越界元素是联合类型的子类型即可的条件,要求元组赋值必须类型和个数都对应。

数组:

  • 每个位置值的类型没有要求
  • 没有越界元素的概念
  • 不会检查对每个元素的操作

类型名称使用小写,与使用首字母大写,有什么区别?(例如 string 与 String )

String:它是 JavaScript 的类型,可以用于创建字符串,它是构造器。

string:它是TypeScript字符串类型,可用于键入变量,参数和返回值,它是字面量。

在声明变量时,有几种方式可以指定变量的类型?(允许在声明变量时进行赋值)

1
2
3
4
var myName:string ='qhw';// 类型=值
var myName:string; // 是 undefined,不允许赋值给 string 之外的其他类型,除了 undefined 和 null
var myName='qhw'; // 如果再赋值给非 string 类型,则报错
var myName; // 允许赋值给任何类型,是undefined

基础类型

1
2
3
4
5
6
7
8
9
let bool:boolean = true;
let num:number = 1;
let string:string = 'string';
let array:Array<number> = [1,2];
let array1:number[] = [2,3];
let obj:Object = {
name:'qinhanwen',
age:'25'
}

在声明的时候,可以为声明的变量设置类型,设置了某个类型之后,就无法再赋值为其他类型。

WX20190308-122836@2x

如果在声明的时候没有设置类型,那么会有个隐性的类型推断,也是不允许再赋值成其他类型的数据。

any

1
2
let any:any = true;
any = "true";

any还有个特点,

1
2
3
4
5
let obj:any = 1;
obj.test();

let obj1:object = {};
obj1.test();//报错

WX20190308-124431@2x

void

1
2
let unusable:void = undefined;
unusable = null;

只能赋值为null或者undefined,也可以用来表示函数没有返回值

1
2
3
function test(): void {
console.log("This is my warning message");
}

nullundefined

默认情况下nullundefined是所有类型的子类型,也就是说可以赋值成其他类型的数据,而不会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let bool:boolean = true;
let num:number = 1;
let string:string = 'string';
let array:Array<number> = [1,2];
let array1:number[] = [2,3];
let obj:Object = {
name:'qinhanwen',
age:'25'
}
bool = null;
num = undefined;
string = null;
array = undefined;
array1 = null;
obj = undefined;

never

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
throw new Error(message);
}

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

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

类型断言:手动的指定一个值的类型

比如有个联合类型,是数值和字符串,会访问联合类型共有的属性,但是数值类型没有length属性。

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

WX20190309-170925@2x

一般会先判断是否存在length属性,再做响应的操作。

1
2
3
4
5
6
7
function getLength(something: string | number): number {
if (something.length) {
return something.length;
} else {
return something.toString().length;
}
}

WX20190309-171743@2x

这时候就需要类型断言

1
2
3
4
5
6
7
function getLength(something: string | number): number {
if ((something as string).length) {
return (something as string).length;
} else {
return something.toString().length;
}
}

或者

1
2
3
4
5
6
7
function getLength(something: string | number): number {
if ((<string>something).length) {
return (<string>something).length;
} else {
return something.toString().length;
}
}

元组

1
let person: [string, number] = ['qinhanwen', 25];

变量声明

let

const

数组解构

对象解构

可选参数

1
2
3
4
function test(a: number = 1, b?: number) {
console.log(a, b);
}
test(1);//1 undefined

展开操作符-对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//
var obj = {
name:'qinhanwen',
age:'25'
}
var obj1 = {...obj};
console.log(obj1);//{ name: 'qinhanwen', age: '25' }

//合并对象
var obj = {
name:'qinhanwen',
}
var obj1 = {
age:25,
...obj
}
console.log(obj1);//{ age: 25, name: 'qinhanwen' }

接口

定义一个接口

1
2
3
4
interface qinhanwen {
name: string,
age?: number,
}

使用这个接口

1
2
3
4
5
function test(info: qinhanwen) {
console.log(info);
}
test({ name: 'qinhanwen', age: 25 });
test({ name: 'qinhanwen' })

age作为可选属性,也就是说这个函数调用的时候,必须传入一个数据结构与定义的接口一致的参数。

只读属性

1
2
3
4
interface qinhanwen {
readonly name: string,
age: number,
}

在属性前面添加readonly,也就是说属性在刚创建的时候才可以修改值。

1
2
3
4
5
6
7
8
9
10
interface qinhanwen {
readonly name: string,
age: number,
}

function test(info: qinhanwen) {
info.age = 24;
info.name = 'zenghua';//这里报错
}
test({ name: 'qinhanwen', age: 25 })

WX20190309-163147@2x

如果还有可能附带其他任意数量的属性,这么定义

1
2
3
4
5
6
7
8
9
interface qinhanwen {
readonly name: string,
age: number,
[propName: string]: any
}
function test(info: qinhanwen) {
console.log(info);
}
test({ name: 'qinhanwen', age: 25, sex: 'male' })//{ name: 'qinhanwen', age: 25, sex: 'male' }

函数类型

1
2
3
4
5
6
7
8
9
interface func {
(name: string, age: number): string;
}

let getAge: func;
getAge = function (name, age) {
return `${name} is ${age} years old`;
}
getAge('qinhanwen', 25);

会对入参,以及返回值做检查。

接口继承

1
2
3
4
5
6
7
8
9
10
11
interface Person {
name: string;
}

interface Child extends Person {
age: number;
}

let child = <Child>{};
child.name = "qinhanwen";
child.age = 25;

继承

修饰符

public

1
2
3
4
5
6
7
8
9
10
11
class Person{
public name:string = 'qinhanwen';
public age:number = 25;
constructor(){

}
public getName(){
console.log(this.name);
}
}
new Person().getName();//qinhanwen

继承可以访问到父类的共有方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person{
public name:string = 'qinhanwen';
public age:number = 25;
constructor(){

}
public getName(){
console.log(this.name);
}
}
class Child extends Person{
constructor(){
super();
}
getParentName(){
super.getName();
}
}
new Child().getParentName();

private

1
2
3
4
5
6
7
8
9
10
11
class Person{
private name:string = 'qinhanwen';
private age:number = 25;
constructor(){

}
private getName(){
console.log(this.name);
}
}
new Person().getName();//报错,getName为私有属性,只能在Person类内使用

并且继承的也无法使用私有变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person{
private name:string = 'qinhanwen';
private age:number = 25;
constructor(){

}
private getName(){
console.log(this.name);
}
}
class Child extends Person{
constructor(){
super();
}
getParentName(){
super.getName();
}
}

如图:

WX20190309-210038@2x

protected

1
2
3
4
5
6
7
8
9
10
11
class Person{
protected name:string = 'qinhanwen';
protected age:number = 25;
constructor(){

}
protected getName(){
console.log(this.name);
}
}
new Person().getName();//报错

WX20190309-215118@2x

继承可以使用父类的属性和方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person{
protected name:string = 'qinhanwen';
protected age:number = 25;
constructor(){

}
protected getName(){
console.log(this.name);
}
}
class Child extends Person{
constructor(){
super();
}
getParentName(){
super.getName();
}
}
new Child().getParentName();

readonly

必须在声明时或者初始化的时候被赋值

存取器

可以使用getters/setters来截取对对象成员的访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person{
_age:number = 25;

set changeAge(age){
this._age = age;
}
get getAge(){
return this._age;
}
}

let person = new Person();
console.log(person.getAge);//25
person.changeAge = 26;
console.log(person.getAge);//26

静态属性

1
2
3
4
5
6
7
class Person {
static myName: string = 'qinhanwen';
public myName1: string = 'qinhanwen';
}

console.log(Person.myName);
console.log(Person.myName1);//报错

如图:

WX20190309-214559@2x

static与上面的public,private还有protected不同,区别在于属性是在类本身上,还是在类的实例上

区分一下static,public,private,protected

public:通过实例访问的属性,派生类也能访问到。

private:只能在类中使用,实例访问不到,派生类也无法访问。

protected:只能在类中使用,实例访问不到,派生类可以访问。

static:这个是静态属性,直接通过类本身才能访问到。

重载

1
2
3
4
5
6
7
8
9
10
11
12
function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
function pickCard(x): any {
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
else if (typeof x == "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13 };
}
}

泛型

不使用泛型的函数

1
2
3
function getName(name:string):string{
return name;
}

使用泛型的函数

1
2
3
function getName<T>(name:T):T{
return name;
}

调用方式:

1)传入参数类型和参数

1
2
3
4
function getName<T>(name:T):T{
return name;
}
getName<string>('qinhanwen');

2)类型推断,根据传入的数据类型,推断T的类型

1
2
3
4
function getName<T>(name:T):T{
return name;
}
getName('qinhanwen');

泛型类

1
2
3
4
5
6
7
8
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

枚举

枚举值和枚举名反向映射

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

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

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

看一下枚举转成es5

1
2
3
4
5
6
7
8
9
10
11
12
enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};
//转换后
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 = {}));

如果改变一下,就会从3开始计数

1
2
3
4
5
6
7
8
9
10
11
12
enum Days {Sun = 3, Mon, Tue, Wed, Thu, Fri, Sat};
//转换后
var Days;
(function (Days) {
Days[Days["Sun"] = 3] = "Sun";
Days[Days["Mon"] = 4] = "Mon";
Days[Days["Tue"] = 5] = "Tue";
Days[Days["Wed"] = 6] = "Wed";
Days[Days["Thu"] = 7] = "Thu";
Days[Days["Fri"] = 8] = "Fri";
Days[Days["Sat"] = 9] = "Sat";
})(Days || (Days = {}));

类型推断

1
2
let num = 1;
num = '1';//报错

没有明确的给出数值的类型,类型推断会提供数值类型。

补充

1️⃣、请简要介绍 TypeScript 的接口( Interface ),它与面向对象语言中的接口有何不同?并说明它与 js 的对象( Object )的关联及区别。

TypeScript 的接口

TypeScript 的接口是一种特殊类型,用于描述对象、类或函数的外观,主要是所包含的属性、属性的类型以及一些高级特征等。

与面向对象语言中的接口的区别

在典型的面向对象语言中,接口是对类的行为与属性的抽象/约束。在使用接口的时候,需要在类的定义上说明实现了哪些接口,并在类内部对该接口的方法、属性进行实现。

与 js 的对象( Object )的关联及区别

TypeScript 的接口与 js 的对象字面量在形式上有相近之处,都是使用一对花括号进行包裹,各个属性分别列出,在冒号左侧是属性的名称,属性允许进行嵌套。

区别在于:接口属性冒号右侧是属性的类型,而对象字面量属性冒号右侧是属性的值。接口的多个属性使用分号进行分隔,而对象字面量使用逗号。

TypeScript 的接口主要用于描述对象的类型,但也可以用于描述类、函数的类型。

2️⃣、请简要介绍接口的可选属性与只读属性,并说明只读属性在代码编写与实际使用时分别有什么特点。

在接口的声明中,属性的冒号之前带有问号(即 ?: )的,是可选属性(无类型的属性,可以直接在属性名称之后单独写一个问号)。它表示,匹配当前接口类型的变量(或类属性等),允许包含该属性,也允许不包含。但如果包含了该可选属性,则必须满足接口声明中定义的类型。

在接口的声明中,属性名称之前带有 readonly 关键字的,就是只读属性。

只读属性在代码编写时的注意事项

在 ts 代码中,只读属性必须在变量/类声明的时候进行赋值(除非该属性同时是可选属性)。而在此后,不允许再对该属性的值进行修改。

readonly 只会对属性自身的只读特性进行约束,但不会约束属性的子属性/后代属性。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Node {
readonly child: {
type: number;
};
}

const node: Node = {
child: {
type: 1,
},
};

node.child = { type: 2 }; // 不允许
node.child.type = 2; // 允许

如果要对属性的子属性以及后代属性进行只读约束,需要在子属性/后代属性上也附加 readonly 关键字。

只读属性在实际使用时的注意事项

实际使用时,即 TypeScript 代码被编译为 JavaScript 代码,并被执行时,只读属性的只读特性会被丢弃。对于该属性的修改不会受到限制,除非使用 Object.defineProperty() 等额外手段进行处理。

3️⃣、请说明以下代码存在的问题,并给出修正的方案(尽量给出多种方案)。

方法传入的参数中包含了 Rectangle 接口中不存在的属性

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
interface Rectangle {
width: number;
height: number;
}

function area(rect: Rectangle) {
return rect.width * rect.height;
}

area({ width: 15, height: 20, type: 'square' });

//解决方案 一
interface Rectangle {
width: number;
height: number;
type?: string // 添加接口定义
}
function area(rect: Rectangle) {
return rect.width * rect.height;
}

area({ width: 15, height: 20, type: 'square' });


//解决方案 二
interface Rectangle {
width: number;
height: number;
}

function area(rect: Rectangle) {
return rect.width * rect.height;
}


let rectangle = { width: 15, height: 20, type: 'square' };// 不使用字面量
area(rectangle);

//解决方案 三
interface Rectangle {
width: number;
height: number;
}

interface Rectangle2 {
type: string
}

function area(rect: Rectangle & Rectangle2) {// 使用交叉类型
return rect.width * rect.height;
}

area({ width: 15, height: 20, type: 'square' });

// 解决方案 四
interface Rectangle {
width: number;
height: number;
}


function area(rect: Rectangle) {
return rect.width * rect.height;
}

area({ width: 15, height: 20, type: 'square' } as any); // 类型断言 或者 as Rectangle

4️⃣、请补齐以下函数返回值类型 MappedType 的接口声明,并说明使用了 TypeScript 接口的哪个特性,以及该特性的适用范围。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface MappedType {
// 请补齐此处的类型声明
}

function customMapping(values: string[]): MappedType {
const result: MappedType = {};
values.forEach(value => result[value] = parseFloat(value) || 0);
return result;
}

// 字符串索引签名 ,如果能够确认对象具有哪些属性的话,可以使用这种 字符串索引 的方式
interface MappedType {
[key: string]: number
}

5️⃣、什么是接口的实现( implements )与派生( extends ),并分别举例说明。

Extends: 在TypeScript里,我们可以使用常用的面向对象模式。 基于类的程序设计中一种最基本的模式是允许使用继承来扩展现有的类。类从基类中继承了属性和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Animal {
move(distanceInMeters: number = 0) {
console.log(`Animal moved ${distanceInMeters}m.`);
}
}

class Dog extends Animal {
bark() {
console.log('Woof! Woof!');
}
}

const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();

和类一样,接口也可以继承。

implements:接口,抽象方法的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Speakable {
speak(): void
}
interface SpeakChinese extends Speakable {
speakChinese(): void
}
class Person implements SpeakChinese {
speak() {
console.log('Person')
}
speakChinese() {
console.log('speakChinese')
}
}

1️⃣、请列举在 TypeScript 中声明函数类型的方式。

1
2
3
4
5
6
7
8
9
10
type fn = () => number;
let test:fn = function(){
return 1;
}

function test1(x: number): number {
return x
}

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

2️⃣、TypeScript 函数的可选参数是什么,在声明时有何要求?在实际的 Javascript 代码中,可选参数是如何实现的?

在TypeScript中,函数假定每个参数都是必需的。这并不意味着不能将其指定为 null 或 undefined,而是在调用该函数时,编译器将检查用户是否已为每个参数提供了一个值。编译器还假设这些参数是唯一将传递给函数的参数。简而言之,赋予函数的参数数量必须与函数期望的参数数量匹配。

通过在参数后添加 ? 符号,就可以让参数变成可选参数,并且可选参数必须跟在必选参数后面才可以

javascript可选参数的实质就是令未被赋值的参数具有一个默认值

1
2
3
4
5
6
7
8
function Person(name, age, height, weight) {
var nHeight = height || 0;
var nWeight = weight || 0;
this.name = name;
this.age = age;
this.height = nHeight ;
this.weight = nWeight ;
}

3️⃣、请简要介绍 Javascript 函数的 this ,并使用简单的代码进行举例。在 TypeScript 代码中,函数的 this 又是如何声明与使用的?

javascript中的this指向主要分为 5 种:

  • 默认绑定: window

  • 显式绑定:bind、apply、call

  • 隐式绑定:调用上下文是个复合类型

  • new绑定:指向实例

  • 箭头函数绑定:声明时候就绑定

typescript 中:

在 typescript 中,函数和方法可以声明 this 类型

1
2
3
4
5
6
7
8
9
10
11
function sayHello(this: void){
// 表示函数体内不允许使用 this
}

在回调中使用
const button = document.querySelector("button");
function handleClick() {// this: HTMLElement 这里的 this 隐式具有 any 类型
console.log("Clicked!");
// 'this' implicitly has type 'any' because it does not have a type annotation.
this.removeEventListener("click", handleClick);
}

4️⃣、请简要介绍“函数签名”。

函数签名:就是一个函数的函数名,参数列表,返回值类型的统称

5️⃣、请简要介绍“函数重载”。 Javascript 是否能实现真正的函数重载? TypeScript 中的函数重载是如何实现的?请使用简单的例子进行说明。

什么是函数重载:

重载函数是函数的一种特殊情况,在同一范围中声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同,也就是说用同一个函数完成不同的功能。这就是重载函数。重载函数常用来实现功能类似而所处理的数据类型不同的问题。不能只有函数返回值类型不同。

在 JavaScript 中,函数不能像传统意义上那样实现重载。后定义的会覆盖前面的。

在 typescript 中,例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let suits = ["hearts", "spades", "clubs", "diamonds"];

function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
function pickCard(x): any {
// Check to see if we're working with an object/array
// if so, they gave us the deck and we'll pick the card
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
// Otherwise just let them pick the card
else if (typeof x == "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13 };
}
}

1️⃣、请简述类( Class )中 thissuper 的作用。

2️⃣、什么是类属性/方法的可访问性?有几种类型,作用分别是什么?

3️⃣、什么是类/对象的访问器属性( Accessors )?请用简单的例子进行说明。

4️⃣、请简述抽象类的特点与意义。

5️⃣、JavaScript/TypeScript 中的类,实际上是使用对象的什么特性实现的?请尝试将以下的 TypeScript 代码改写为等效的 ES5 代码。

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
class Rectangle {
constructor(public width: number, public height: number) {

}

static formatResult(result: number): number {
return Math.round(result * 100) / 100;
}

area(): number {
return Rectangle.formatResult(this.width * this.height);
}
}

class Square extends Rectangle {
constructor(public sideLength: number) {
super(sideLength, sideLength);
}

area(): number {
if (this.width !== this.height) {
return NaN;
}

return super.area();
}
}