0%

TypeScript 学习笔记

TypeScript笔记

基础类型

unknown类型

unknown 类型用于解决any类型使用过于宽松的问题, unknown可以赋任何值,但是unknown不能赋值给其他参数(any和unknown除外)
any类型甚至可以是function,但是unknown类型在赋值之前无法当做方法或者数组来使用

tuple元祖

TS的数据要求是同种类型的值组成,当需要不同类型的值组成组数时,就需要用到tuple
元祖需要在申明变量时就固定值类型与数量比如:

1
2
// 不能不符合类型,也不能多不能少
const tupleValue: [string, boolean, number] = ['abc', false, 123];

object,Object,{}的区别

object是一个数据类型,可用于声明变量,但是微软现在不建议这样使用,而是建议使用Record<string, unknown>

Object 是所有 Object 类的实例类型,与JS的Object使用类似

{} 是一个没有成员的对象,是一个具体的对象数据,不是一个类型

Never 类型

never 表示那些用不存在的值的类型,常用于断言或者保证方法逻辑性
使用 never 避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码

TypeScript 断言

可以在变量的前面加上,或者在变量后加上 as type,强调变量的类型.
如果想变量不为 undefined 或者 null,可以在赋值的时候后面加上!,例如:

1
2
3
4
function myFunc(maybeString: string | undefined | null) {
const onlyString: string = maybeString; // Error 不能将类型“string | null | undefined”分配给类型“string”。不能将类型“undefined”分配给类型“string”。ts(2322)
const ignoreUndefinedAndNull: string = maybeString!; // Ok
}

交叉类型

TS 中可以通过&符号将 2 个类型合并成一个新类型

1
2
3
4
5
6
7
type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };

let point: Point = {
x: 1,
y: 1
}

但是如果有同名变量但是类型(基础类型)不一致时,该变量类型会变成 never
如果是非基础类型,例如重新构建的对象类型,就可以同时接受多种类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface D { d: boolean; }
interface E { e: string; }
interface F { f: number; }

interface A { x: D; }
interface B { x: E; }
interface C { x: F; }

type ABC = A & B & C;

// 此时x 可以接受 boolean,string,number三种类型
let abc: ABC = {
x: {
d: true,
e: 'semlinker',
f: 666
}
};

console.log('abc:', abc);

TypeScript 函数

与 JavaScript 的区别

ts 函数参数需要声明类型,函数的返回值类型也需要声明,可选参数也需要声明

1
2
3
4
// 这里声明了age 是可选参数,性别的默认值是女性,函数无返回值
// 可选参数一般放在最后位置
function foo(name: string, gender: string = 'female, age?: number): void {
}

有了严格的方法参数定义后,TS 就有了函数重载

1
2
3
4
5
6
7
8
9
10
11
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a: Combinable, b: Combinable) {
// type Combinable = string | number;
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}

TypeScript 接口

TS 比 JS 更加的面向对象,它是支持接口 interface 的.

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

let semlinker: Person = {
name: "semlinker",
age: 33,
};

也可以在声明接口的时候设定变量为可读(readonly),同时也可以设置可选(!)变量

interface 与 type 的区别

两者都是用来描述对象的形状或者函数签名,与接口类型不一样,类型别名可以用于一些其他类型,比如原始类型、联合类型和元组,interface 主要用于描述对象和函数

1
2
3
4
5
6
7
8
9
10
11
12
// primitive
type Name = string;

// object
type PartialPointX = { x: number; };
type PartialPointY = { y: number; };

// union
type PartialPoint = PartialPointX | PartialPointY;

// tuple
type Data = [number, string];

与类型别名不同,接口可以定义多次,会被自动合并为单个接口。

1
2
3
4
interface Point { x: number; }
interface Point { y: number; }

const point: Point = { x: 1, y: 2 };

继承extends与实现Implements

extends

TS 对象的继承就类似于& 交叉合并,但是 extends 可读性更好

1
2
3
4
5
6
7
interface PartialPointX { x: number; }
interface Point extends PartialPointX {
y: number;
};
// 等价于
type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };

不过一般情况下extends更加倾向于一个对象或者方法继承了父类,是面向对象的一种理念,而不是单纯的交叉合并.

Implements

实现 interface,但类不能实现使用类型别名定义的联合类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Point {
x: number;
y: number;
}

class SomePoint implements Point { // OK
x = 1;
y = 2;
}


type PartialPoint = { x: number; } | { y: number; };
// A class can only implement an object type or
// intersection of object types with statically known members.
class SomePartialPoint implements PartialPoint { // Error
x = 1;
y = 2;
}

TypeScript 的面向对象

首先是支持静态变量和静态方法 static
静态就是所有的实例对象都公共的,例如:

1
2
3
4
5
class Person {
static time: number;
name: string
};
// 这里所有的人类实例都共享一个共同的 time 变量,但是 name 变量就是每个实例自己的,方法也是同理,而且静态变量和静态方法一般不要改动

11.2 ECMAScript 私有字段

在 TypeScript 3.8 版本就开始支持ECMAScript 私有字段,使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
#name: string;

constructor(name: string) {
this.#name = name;
}

greet() {
console.log(`Hello, my name is ${this.#name}!`);
}
}

let semlinker = new Person("Semlinker");

semlinker.#name; // ERROR
// Property #name' is not accessible outside class 'Person'
// because it has a private identifier.

与常规属性(甚至使用 private 修饰符声明的属性)不同,私有字段要牢记以下规则:

  1. 私有字段以 # 字符开头,有时我们称之为私有名称;
  2. 每个私有字段名称都唯一地限定于其包含的类;
  3. 不能在私有字段上使用 TypeScript 可访问性修饰符(如 public 或 private);
  4. 私有字段不能在包含的类之外访问,甚至不能被检测到。

private 与# 的区别

私有变量可以使用 getter 和 setter 方法来访问或者修改私有变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let passcode = "Hello TypeScript";

class Employee {
private _fullName: string;

get fullName(): string {
return this._fullName;
}

set fullName(newName: string) {
if (passcode && passcode == "Hello TypeScript") {
this._fullName = newName;
} else {
console.log("Error: Unauthorized update of employee!");
}
}
}

let employee = new Employee();
employee.fullName = "Semlinker";
if (employee.fullName) {
console.log(employee.fullName);
}

TypeScript 支持抽象 abstract

使用 abstract 关键字声明的类,我们称之为抽象类。抽象类不能被实例化,因为它里面包含一个或多个抽象方法。所谓的抽象方法,是指不包含具体实现的方法

1
2
3
4
5
6
7
8
abstract class Person {
constructor(public name: string){}

abstract say(words: string) :void;
}

// Cannot create an instance of an abstract class.(2511)
const lolo = new Person(); // Error

抽象类不能被直接实例化,我们只能实例化实现了所有抽象方法的子类。具体如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
abstract class Person {
constructor(public name: string){}

// 抽象方法
abstract say(words: string) :void;
}

class Developer extends Person {
constructor(name: string) {
super(name);
}

say(words: string): void {
console.log(`${this.name} says ${words}`);
}
}

const lolo = new Developer("lolo");
lolo.say("I love ts!"); // lolo says I love ts!

TypeScript 类方法支持重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ProductService {
getProducts(): void;
getProducts(id: number): void;
getProducts(id?: number) {
if(typeof id === 'number') {
console.log(`获取id为 ${id} 的产品信息`);
} else {
console.log(`获取所有的产品信息`);
}
}
}

const productService = new ProductService();
productService.getProducts(666); // 获取id为 666 的产品信息
productService.getProducts(); // 获取所有的产品信息

泛型语法

泛型顾名思义就是广泛的类型,不特指某种类型.语法上表现为.例如:

1
2
3
4
5
6
function identity <T, U>(value: T, message: U) : T {
console.log(message);
return value;
}

console.log(identity<Number, string>(68, "Semlinker"));

上面代码中的<T, U> 其实只是一个占位符,或者说是一个类型参数,当调用 identity 方法时,传入该位置的<Number, string>就取代了方法中 T 和 U 的位置,也就是说identity 方法就变成了:

1
2
3
4
function identity <Number, string>(value: Number, message: string) : Number {
console.log(message);
return value;
}

因为泛型只是个占位符,所以定义的时候用什么符号其实都可以,你也可以用<A><B><C>, 但是使用上习惯用<T>. 表示 Type
除了 T 之外,以下是常见泛型变量代表的意思:

  • K(Key):表示对象中的键类型;
  • V(Value):表示对象中的值类型;
  • E(Element):表示元素类型。

泛型可以使用在接口,类,方法.

装饰器

装饰器是什么?

首先看代码例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Greeter(greeting: string) {
return function (target: Function) {
target.prototype.greet = function (): void {
console.log(greeting);
};
};
}

@Greeter("Hello TS!")
class Greeting {
constructor() {
// 内部实现
}
}

let myGreeting = new Greeting();
(myGreeting as any).greet(); // console output: 'Hello TS!';


// 这里的@Greeter 就类似于当前的 Greeting 类继承了@Greeter

需要注意的是,若要启用实验性的装饰器特性,你必须在命令行或 tsconfig.json 里启用 experimentalDecorators 编译器选项:

命令行:

1
tsc --target ES5 --experimentalDecorators

tsconfig.json

1
2
3
4
5
6
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}

装饰器分类

装饰器大致上可以分为以下几种:

  1. 类装饰器
  2. 属性装饰器
  3. 方法装饰器
  4. 方法参数装饰器

类装饰器

对类使用,可传入一个隐式参数作为类的构造参数,或者不传
但是默认会将被装饰的类做为参数传入

1
2
3
4
5
6
7
8
9
10
11
12
function logClz(注意:参数装饰器只能用来监视一个方法的参数是否被传入;:any) {
params.prototype.url = 'xxxx';
params.prototype.run = function() {
console.log('run...');
};
}
@logClz // 这里将 HttpClient 类传到 logClz
class HttpClient {
constructor() { }
}
var http:any = new HttpClient();
http.run(); // run...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function logClz(params:string) {
console.log('params:', params); //params: hello
return function(target:any) {
console.log('target:', target); //target: class HttpClient
target.prototype.url = params; //扩展一个url属性
}
}
// 传入参数,target 就是 HttpClient
@logClz('hello')
class HttpClient {
constructor() { }
}
var http:any = new HttpClient();
console.log(http.url); //hello

属性装饰器

对类的属性装饰的叫属性装饰器,有 2 个隐式参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员来说是类的原型对象;其实就是当前类
  2. 属性成员名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function logProp(params:any) {
return function(target:any, attr:any) {
console.log(target) // { constructor:f, getData:f } (HttpClient)
console.log(attr) // url
target[attr] = params; //通过原型对象修改属性值 = 装饰器传入的参数
target.api = 'xxxxx'; //扩展属性
target.run = function() { //扩展方法
console.log('run...');
}
}
}
class HttpClient {
@logProp('http://baidu.com') // target: HttpClient, attr: url
public url:any|undefined;
constructor() { }
getData() {
console.log(this.url);
}
}
var http:any = new HttpClient();
http.getData(); // http://baidu.com
console.log(http.api); // xxxxx
http.run(); // run...

方法装饰器

  • 方法装饰器被应用到方法的属性描述符上,可以用来监视、修改、替换方法的定义;
  • 方法装饰器会在运行时传入3个参数:
  1. 对于静态成员来说是类的构造函数,对于实例成员来说是类的原型对象;(当前类)
  2. 成员的名字;(方法名)
  3. 成员的属性描述符;({value: ƒ, writable: true, enumerable: false, configurable: true} value就是方法体)
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
function get(params:any) {
console.log(params) // 装饰器传入的参数:http://baidu.com
return function(target:any, methodName:any, desc:any) {
console.log(target) // { constructor:f, getData:f }
console.log(methodName) // getData
console.log(desc) // {value: ƒ, writable: true, enumerable: false, configurable: true} value就是方法体
/* 修改被装饰的方法 */
//1. 保存原方法体
var oldMethod = desc.value;
//2. 重新定义方法体
desc.value = function(...args:any[]) {
//3. 把传入的数组元素都转为字符串
let newArgs = args.map((item)=>{
return String(item);
});
//4. 执行原来的方法体
oldMethod.apply(this, newArgs);
// 等效于 oldMethod.call(this, ...newArgs);
}
}
}
class HttpClient {
constructor() { }
@get('http://baidu.com')
getData(...args:any[]) {
console.log('getData: ', args);
}
}
var http = new HttpClient();
http.getData(1, 2, true); // getData: ["1", "2", "true"]

方法参数装饰器

  • 参数装饰器表达式会在运行时被调用,可以为类的原型增加一些元素数据,传入3个参数:
  1. 对于静态成员来说是类的构造函数,对于实例成员来说是类的原型(类)
  2. 方法名称,如果装饰的是构造函数的参数,则值为undefined(functionName)
  3. 参数在函数参数列表中的索引;(参数下标)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function logParams(params:any) {
console.log(params) // 装饰器传入的参数:uuid
return function(target:any, methodName:any, paramIndex:any) {
console.log(target) // { constructor:f, getData:f }
console.log(methodName) // getData
console.log(paramIndex) // 0
}
}
class HttpClient {
constructor() { }
getData(@logParams('uuid') uuid:any) {
console.log(uuid);
}
}

const http = new HttpClient();
http.getData();
  • 注意:参数装饰器只能用来监视一个方法的参数是否被传入;

装饰器的执行顺序

  • 装饰器组合:TS支持多个装饰器同时装饰到一个声明上,语法支持从左到右,或从上到下书写;
1
2
3
4
5
@f @g x

@f
@g
x
  • 在TypeScript里,当多个装饰器应用在一个声明上时会进行如下步骤的操作:

    1. 由上至下依次对装饰器表达式求值;
    2. 求值的结果会被当作函数,由下至上依次调用.
  • 不同装饰器的执行顺序:属性装饰器 > 方法装饰器 > 参数装饰器 > 类装饰器

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
function logClz11(params:string) {
return function(target:any) {
console.log('logClz11')
}
}
function logClz22(params?:string) {
return function(target:any) {
console.log('logClz22')
}
}
function logAttr(params?:string) {
return function(target:any, attrName:any) {
console.log('logAttr')
}
}
function logMethod(params?:string) {
return function(target:any, methodName:any, desc:any) {
console.log('logMethod')
}
}
function logParam11(params?:any) {
return function(target:any, methodName:any, paramIndex:any) {
console.log('logParam11')
}
}
function logParam22(params?:any) {
return function(target:any, methodName:any, paramIndex:any) {
console.log('logParam22')
}
}

@logClz11('http://baidu.com')
@logClz22()
class HttpClient {
@logAttr()
public url:string|undefined;

constructor() { }

@logMethod()
getData() {
console.log('get data');
}

setData(@logParam11() param1:any, @logParam22() param2:any) {
console.log('set data');
}
}
// logAttr --> logMethod --> logParam22 --> logParam11 --> logClz22 --> logClz11

装饰器参考链接