TypeScript是什么

  • TS是JS的超集,所以JS基础的类型都包含在内

TypeScript开发环境搭建

  1. 全局安装typescript:
1
npm install typescript -g
  1. 查看typescript当前版本号:
1
tsc -v
  1. 将TS编译成JS( TS 不能直接在浏览器执行,需要编译器将TS转为JS)
1
tsc xxx.ts

基础数据类型

  • 基础类型:Number、String、Boolean、null、undefined以及 ES6 的 Symbol 和 ES10 的 BigInt

字符串类型

1
2
3
4
5
6
7
8
let str: string = "这是字符串类型";
//上方我们将str这个变量定义为了string类型,如果对他输入其他类型的内容就会报错,例如:

let str1: string = 666;
//这个就会报错了,会提示你不能将类型"number"分配给类型"string"

let muban: string = `web${str}`;
//我们也可以使用ES6中的模板字符串

数字类型

支持十六进制、十进制、八进制和二进制

1
2
3
4
5
6
7
let num: number = 123;//普通数字
let notANumber: number = NaN;//Nan
let infinityNumber: number = Infinity;//无穷大
let decimal: number = 6;//十进制
let hex: number = 0xf00d;//十六进制
let binary: number = 0b1010;//二进制
let octal: number = 0o744;//八进制

布尔类型

1
2
3
let b:boolean = false;
let b2:boolean = Boolean(1); //隐式转换
console.log(`b:${b},b2:${b2}`); //b:false,b2:true

空值类型

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

1
2
3
4
5
6
7
8
9
10
function fnVoid(): void {
console.log("test void");
return; //可以这样后面啥也不跟
// 函数也可以定义为空值,如果定义了void则不能返回内容,否则就会报错,例如:
}

function fnVoid2(): void {
return 123;
//这个就会报错了,会提示你不能将类型“number”分配给类型“void”
}

void 类型的用法,主要是用在我们不希望调用者关心函数返回值的情况下,比如通常的异步回调函数

void也可以定义undefined 和 null类型

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

TIPS:

如果你配置了tsconfig.json 开启了严格模式: 则 null 不能 赋予 void 类型

1
2
3
4
5
{
"compilerOptions":{
"strict": true
}
}

void

解决方法:

  1. tsc --init创建tsconfig文件;
  2. tsconfig.json文件内搜索strict,值改为false

Null和undefined类型

1
2
let u: undefined = undefined;//定义undefined
let n: null = null;//定义null

void 和 undefined 和 null 最大的区别

void的内容也是不能去赋值给别人的

1
2
3
4
5
6
let u: void = undefined;
let n: null = null;

let str: string = "123";
str = u;
// 这个就会报错了,会提示你不能将类型“void”分配给类型“string”

void

下面这样undefined与null可以赋值

1
2
3
4
5
let u: undefined = undefined;
let n: null = null;

let str: string = "123";
str = u;

NodeJs 环境执行TS

上面每次执行代码,都需要将TS编译成JS,然后再通过NodeJs来执行JS,有点繁琐。

NodeJs直接运行TS需要安装两个依赖:

  • npm install @types/node -D(node环境支持的依赖必装)
  • npm install ts-node -g
  • 输出版本号:ts-node -v
  • NodeJs执行TS:ts-node xxx.ts

Any 类型 和 unknown 顶级类型

any与unknown没有强制限定哪种类型,可以定义任何类型的值,被称为顶级类型(top type)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//any 可以定义任何类型的值
let anys: any;
anys = true; // OK
anys = 18; // OK
anys = "Hello World"; // OK
anys = []; // OK
anys = {}; // OK
anys = null; // OK
anys = undefined; // OK
anys = Symbol("type"); // OK

//unknown 可以定义任何类型的值
let value: unknown;

value = true; // OK
value = 18; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK
value = null; // OK
value = undefined; // OK
value = Symbol("type"); // OK

any与unknow区别

TypeScript 3.0中引入的 unknown 类型也被认为是 top type ,但它更安全。与 any 一样,所有类型都可以分配给unknown

unknow类型比any更加严格当你要使用any 的时候可以尝试使用unknow

  1. unknown可赋值对象只有unknown 和 any
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//unknown类型不能赋值给unknown 和 any之外的其他类型
let names: unknown = '123';
let names2: string = names; // 不能将类型“unknown”分配给类型“string”。


//这样就没问题 any类型是可以的
let ages: any = 123;
let ages2: string = ages;


//unknown可赋值对象只有unknown 和 any
let aaa: unknown = '123';
let bbb: unknown = '456';
let ccc: any = "789";

aaa = bbb;
aaa = ccc;
  1. unknow 不能调用属性和方法
1
2
3
4
5
6
7
8
9
//如果是any类型在对象没有这个属性的时候还在获取是不会报错的
let obj: any = { b: 1 };
obj.a;


//如果是unknow 不能调用属性和方法
let obj2: unknown = { b: 1, ccc: (): number => 213 };
obj2.b; //类型“unknown”上不存在属性“b”。
obj2.ccc(); //类型“unknown”上不存在属性“ccc”。
  1. 声明变量的时候没有指定任意类型默认为any(如果使用any 就失去了TS类型检测的作用)

接口和对象类型

interface定义对象类型

在JS中,我们声明对象,直接写就好了

1
2
3
4
let obj = {
name: "fsllala",
age: 18
}

在TS中,我们定义对象的方式要用关键字interface(接口),我的理解是使用interface来定义一种约束(类型),让数据的结构满足约束的格式。定义方式如下:

  • 有点类似于的写法
  1. 使用接口约束的时候不能多一个属性也不能少一个属性,必须与接口保持一致
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
*这样写是会报错的 因为我们在Person定义了name、age 但是对象里面缺少age属性
*使用接口约束的时候不能多一个属性也不能少一个属性
*必须与接口保持一致
*/
interface Person{
name:string,
age:number
}

// 这个就会报错了,会提示 类型 "{ name: string; }" 中缺少属性 "age",但类型 "Person" 中需要该属性。
const person:Person={
name:"fsllala"
}
  1. 重名interface 可以合并
1
2
3
4
5
6
7
8
9
10
11
12
13
interface A {
name: string
}

interface A {
age: number
}

let obj: A = {
name: "fsllala",
age: 18
};
console.log(obj); //{ name: 'fsllala', age: 18 }
  1. 可选属性 使用?操作符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//可选属性的含义是该属性可以不存在
interface Person {
name: string,
age?: number
}
//所以说这样写也是没问题的
const person: Person = {
name: "fsllala"
}
//当然这样写也没问题
const person2: Person = {
name: "fsl",
age: 18
}
  1. 索引签名[propName: string]

后端给我们返回的字段不确定,类型也不确定的时候可以用任意属性;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 在这个例子当中我们看到接口中并没有定义ccc和ddd但是并没有报错
* 因为我们定义了[propName: string]: any;
* 允许添加新的任意属性
*/
interface Person {
name: string,
age: number,
[propName: string]: any
}

const person: Person = {
name: "fsl",
age: 18,
ccc: 123,
ddd:'456',
}
  1. 只读属性 readonly

readonly 只读属性是不允许被赋值的只能读取;(跟input的readonly属性类似);

常用于函数和后台返回的数据ID

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]: any,
readonly cb: () => boolean
}

const person: Person = {
id: 2,
name: "fsl",
age: 18,
ccc: 123,
cb: () => {
return false
}
}

//这样写是会报错的 会提示: 无法为“id”赋值,因为它是只读属性。
person.id = 3;
  1. 添加函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface Person {
name: string,
readonly age: number,
[propName: string]: any,
cb(): number, //定义函数和函数返回值
foo: () => boolean //定义函数和函数返回值
}

const person: Person = {
name: "fsl",
age: 18,
ccc: 123,
dd: 111,
cb: () => {
return 456
},
foo: () => {
return false
}
}
  1. 继承

和ES6中类的继承类似;

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

interface B extends A {
age: number
}

let obj: B = {
name: "fsllala",
age: 18
};

interface定义函数类型

1
2
3
4
5
6
7
8
9
10
// interface定义函数格式: (参数列表):返回值
interface Fn {
// 函数是个方法,所以是 ();返回一个 number类型的数组;参数是name,为string类型
(name: string): number[]
}


const foo: Fn = function (name: string) {
return [1, 2, 3, 4]
}

数组类型

数组类型有多种声明方法:

  1. 普通方式:类型[ ]
1
2
3
4
let arr: number[] = [1, 2, 3];  //数组类型的数组
let arr2: string[] = ["1", "2", "3"]; //字符串类型的数组
let arr3: boolean[] = [true, false];//布尔类型的数组
let arr4: any[] = [1, "2", true,[],{}]; //任意类型的数组
  1. 数组泛型 :Array<类型>
1
2
3
4
let arr: Array<number> = [1, 2, 3];  //数组类型的数组
let arr2: Array<string> = ["1", "2", "3"]; //字符串类型的数组
let arr3: Array<boolean> = [true, false];//布尔类型的数组
let arr4: Array<any> = [1, "2", true, [], {}]; //任意类型的数组
  1. 用接口表示数组:一般用来描述类数组
1
2
3
4
5
6
interface NumArray {
[index: number]: number
}

let arr: NumArray = [1, 2, 3];
//表示:只要索引的类型是数字时,那么值的类型必须是数字。

对象数组

使用interface方式来定义对象数组

1
2
3
4
5
6
interface X {
name: string,
age?: number
}
// 表示对象类型的数组
const arr1: X[] = [{ name: "fsllala", age: 18 }, { name: "fsl" }];

多维数组

1
2
3
4
5
6
//普通写法
let arr: number[][] = [[1, 2], [3, 4]]; //二维数组
let arr2: number[][][] = [[[1]], [[2]]];//三维数组
//泛型写法
let arr3: Array<Array<number>> = [[1, 2], [3, 4]]; //二维数组
let arr4: Array<Array<Array<number|string>>> = [[[1]], [[2,"lala"]]];//三维数组

arguments类数组

函数内置的arguments,是个类数组,其实就是有length属性的对象;所以没有数组的方法:push、pop…

1
2
3
4
5
6
function Arr(...args: any): void {
console.log(arguments); // [Arguments] { '0': 2, '1': 3, '2': 4 } 伪(类)数组(没有数组的方法:push、pop...)
console.log(args); // [ 2, 3, 4 ] 数组
}

Arr(2, 3, 4);

所以就会导致如下情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Arr(...args: any): void {
//错误的arguments 是类数组不能这样定义
let arr: number[] = arguments; // 这样写是会报错的 会提示: 类型“IArguments”缺少类型“number[]”的以下属性: pop, push, concat, join 及其他 27 项。
let arr2: number[] = args; // 这个是可以的

//ts内置对象IArguments 定义
let arr3:IArguments=arguments; //类数组用IArguments
}

Arr(2, 3, 4);


//其中 IArguments 是 TypeScript 中定义好了的类型,它实际上就是:
interface IArguments {
[index: number]: any;
length: number;
callee: Function;
}

函数拓展

  1. 匿名函数可以进行类型推导,不推荐在写类型,因为可能会写错;
1
2
3
4
5
6
const arr1 = ["lala", "wawa", "miaomiao"];

// 匿名函数不建议手写写类型
arr1.forEach((item, index) => {
console.log(item, index);
})
  1. interface定义函数类型;
1
2
3
4
5
6
7
8
9
// 格式: (参数列表):返回值
interface Ikun {
(name: string): string
}

const kunkun: Ikun = (aa: string): string => {
return aa;
}
kunkun("篮球");
  1. 函数的调用签名(从对象的角度看待这个函数,也可以有其他属性);
1
2
3
4
5
6
7
8
9
10
11
12
13
interface IBar {
names: string,
age: number,
(num: number): number
}
const bar2: IBar = (arg: number): number => {
return arg;
}

bar2.names = "fsllala";
bar2.age = 18;

console.log(bar2(222))
  1. type定义函数类型;
1
2
3
4
5
6
7
8
// 函数类型表达式 格式: (参数列表)=>返回值
type Bar = (num1: number) => number;

const bar: Bar = (arg: number): number => {
return arg;
}

console.log(bar(111)); // 111
  1. 参数不能多传,也不能少传 必须按照约定的类型来
1
2
3
4
5
6
7
const fn = function (name: string, age: number): string {
return name + age;
}
//表示:实参name是string,age是number类型,函数的返回值是 string类型;

let result = fn("fsllala", 18);
console.log(result); // fsllala18
  1. 少传参数:定义默认的参数
  • ES6默认实参
1
2
3
4
5
6
7
const fn = function (name: string, age: number = 24): string {
return name + age;
}
//表示:实参name是string,age是number类型,函数的返回值是 string类型;

let result = fn("fsllala");
console.log(result); //fsllala24
  • 可选属性 使用?操作符(不传默认为undefined)
1
2
3
4
5
6
7
const fn = function (name: string, age?: number): string {
return name + age;
}
//表示:实参name是string,age是number类型,函数的返回值是 string类型;

let result = fn("fsllala");
console.log(result); //fsllalaundefined
  • 接口定义函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface User {
name: string,
age: number
}

const fn = function (user: User): User {
return user;
}

//表示:实参是定义的 接口User类型,函数的返回值是 传入的参数;
let result = fn({
name: "fsllala",
age: 18
})

console.log(result); // { name: 'fsllala', age: 18 }
  • 函数重载

函数重载:方法名字相同,而参数不同,与返回值无关。

如果参数类型不同,则参数类型应设置为 any

参数数量不同你可以将不同的参数设置为可选。

1
2
3
4
5
6
7
8
9
10
function fn(params: number): void
function fn(params: string, params2: number): void
// 上面定义的两个是重载函数,下面是执行函数 (遵循上面两个重载函数,执行函数是写逻辑的)
function fn(params: any, params2?: any): void {
console.log(params)
console.log(params2)
}

fn(123)
fn('123',456)

Tip:JS好像是不能这样写的,JS重载是通过函数内部的arguments的长度来实现的;

类型断言 | 联合类型 | 交叉类型

联合类型

可以是多种类型,中间用|隔开

1
2
3
4
5
6
7
//例如我们的手机号通常是17XXXXXXX 为数字类型 这时候产品说需要支持座机
//所以我们就可以使用联合类型支持座机字符串
let myPhone: number | string = '010-820'


//这样写是会报错的应为我们的联合类型只有数字和字符串并没有布尔值
let myPhone2: number | string = true

函数使用联合类型

1
2
3
4
5
6
7
8
const fn1 = (types:number | boolean):boolean => {
return !!types
}

const result =fn1(1);
const result2 = fn1(false);
console.log(result); // true
console.log(result2); // false

交叉类型

类似于extends

多种类型的集合,联合对象将具有所联合类型的所有成员,中间用&隔开;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface People {
age: number,
height: number
}
interface Man {
sex: string
}
const fsllala = (man: People & Man) => {
console.log(man.age)
console.log(man.height)
console.log(man.sex)
}
fsllala({
age: 18,
height: 173,
sex: 'male'
});

类型断言

在使用联合类型的时候,可能会遇到某个属性A类型没有,B类型有,比如length属性number没有string有

1
2
3
4
5
let fn1 = function(params:number|string):void{
console.log(params.length); // 类型“number”上不存在属性“length”
}

fn1("123456");

这个时候就得用到类型断言:需要注意的是,类型断言只能够「欺骗」TypeScript 编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误:

1
as 类型  或  <类型>值  value as string  <string>value
1
2
3
4
5
6
let fn1 = function (params: number | string): void {
console.log((params as string).length);
}

fn1("123456"); //6
fn1(123456); //undefined

使用any临时断言

1
2
window.abc = 123
//这样写会报错因为window没有abc这个东西
1
2
(window as any).abc = 123
//可以使用any临时断言在 any 类型的变量上,访问任何属性都是允许的。

内置对象

JavaScript 中有很多内置对象,它们可以直接在 TypeScript 中当做定义好了的类型。

ECMAScript 的内置对象

Boolean、Number、string、RegExp、Date、Error

1
2
3
4
5
6
7
8
9
10
11
12
let b: Boolean = new Boolean(1)
console.log(b)
let n: Number = new Number(true)
console.log(n)
let s: String = new String('lala~')
console.log(s)
let d: Date = new Date()
console.log(d)
let r: RegExp = /^1/
console.log(r)
let e: Error = new Error("error!")
console.log(e)

DOM 和 BOM 的内置对象

DocumentHTMLElementEventNodeList

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
let body: HTMLElement = document.body;
let allDiv: NodeList = document.querySelectorAll('div');
//读取div 这种需要类型断言 或者加个判断应为读不到返回null
let div:HTMLElement = document.querySelector('div') as HTMLDivElement
document.addEventListener('click', function (e: MouseEvent) {

});
//dom元素的映射表
interface HTMLElementTagNameMap {
"a": HTMLAnchorElement;
"abbr": HTMLElement;
"address": HTMLElement;
"applet": HTMLAppletElement;
"area": HTMLAreaElement;
"article": HTMLElement;
"aside": HTMLElement;
"audio": HTMLAudioElement;
"b": HTMLElement;
"base": HTMLBaseElement;
"bdi": HTMLElement;
"bdo": HTMLElement;
"blockquote": HTMLQuoteElement;
"body": HTMLBodyElement;
"br": HTMLBRElement;
"button": HTMLButtonElement;
"canvas": HTMLCanvasElement;
"caption": HTMLTableCaptionElement;
"cite": HTMLElement;
"code": HTMLElement;
"col": HTMLTableColElement;
"colgroup": HTMLTableColElement;
"data": HTMLDataElement;
"datalist": HTMLDataListElement;
"dd": HTMLElement;
"del": HTMLModElement;
"details": HTMLDetailsElement;
"dfn": HTMLElement;
"dialog": HTMLDialogElement;
"dir": HTMLDirectoryElement;
"div": HTMLDivElement;
"dl": HTMLDListElement;
"dt": HTMLElement;
"em": HTMLElement;
"embed": HTMLEmbedElement;
"fieldset": HTMLFieldSetElement;
"figcaption": HTMLElement;
"figure": HTMLElement;
"font": HTMLFontElement;
"footer": HTMLElement;
"form": HTMLFormElement;
"frame": HTMLFrameElement;
"frameset": HTMLFrameSetElement;
"h1": HTMLHeadingElement;
"h2": HTMLHeadingElement;
"h3": HTMLHeadingElement;
"h4": HTMLHeadingElement;
"h5": HTMLHeadingElement;
"h6": HTMLHeadingElement;
"head": HTMLHeadElement;
"header": HTMLElement;
"hgroup": HTMLElement;
"hr": HTMLHRElement;
"html": HTMLHtmlElement;
"i": HTMLElement;
"iframe": HTMLIFrameElement;
"img": HTMLImageElement;
"input": HTMLInputElement;
"ins": HTMLModElement;
"kbd": HTMLElement;
"label": HTMLLabelElement;
"legend": HTMLLegendElement;
"li": HTMLLIElement;
"link": HTMLLinkElement;
"main": HTMLElement;
"map": HTMLMapElement;
"mark": HTMLElement;
"marquee": HTMLMarqueeElement;
"menu": HTMLMenuElement;
"meta": HTMLMetaElement;
"meter": HTMLMeterElement;
"nav": HTMLElement;
"noscript": HTMLElement;
"object": HTMLObjectElement;
"ol": HTMLOListElement;
"optgroup": HTMLOptGroupElement;
"option": HTMLOptionElement;
"output": HTMLOutputElement;
"p": HTMLParagraphElement;
"param": HTMLParamElement;
"picture": HTMLPictureElement;
"pre": HTMLPreElement;
"progress": HTMLProgressElement;
"q": HTMLQuoteElement;
"rp": HTMLElement;
"rt": HTMLElement;
"ruby": HTMLElement;
"s": HTMLElement;
"samp": HTMLElement;
"script": HTMLScriptElement;
"section": HTMLElement;
"select": HTMLSelectElement;
"slot": HTMLSlotElement;
"small": HTMLElement;
"source": HTMLSourceElement;
"span": HTMLSpanElement;
"strong": HTMLElement;
"style": HTMLStyleElement;
"sub": HTMLElement;
"summary": HTMLElement;
"sup": HTMLElement;
"table": HTMLTableElement;
"tbody": HTMLTableSectionElement;
"td": HTMLTableDataCellElement;
"template": HTMLTemplateElement;
"textarea": HTMLTextAreaElement;
"tfoot": HTMLTableSectionElement;
"th": HTMLTableHeaderCellElement;
"thead": HTMLTableSectionElement;
"time": HTMLTimeElement;
"title": HTMLTitleElement;
"tr": HTMLTableRowElement;
"track": HTMLTrackElement;
"u": HTMLElement;
"ul": HTMLUListElement;
"var": HTMLElement;
"video": HTMLVideoElement;
"wbr": HTMLElement;
}

定义Promise

如果我们不指定返回的类型TS是推断不出来返回的是什么类型

promise

1
2
3
4
5
function promise(){
return new Promise((resolve, reject) => {
resolve(1)
})
}

指定返回的类型

promise

1
2
3
4
5
function promise(){
return new Promise<number>((resolve, reject) => {
resolve(1)
})
}

函数定义返回promise 语法规则:Promise<T>

1
2
3
4
5
6
7
8
9
function promise(): Promise<number> {  //  这里定义了 Promise<number> 
return new Promise<number>((resolve, reject) => {
resolve(1)
})
}

promise().then(res => {
console.log(res); // 1
})

Class类

ES6提供了更接近传统语言的写法,引入了Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。基本上,ES6的class可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。详见:面向对象

定义

在TypeScript是不允许直接在constructor 定义变量的

1
2
3
4
5
6
7
8
9
10
class Person {
constructor(name: string, age: number, sub: boolean) {
//这里为了更好的区分,采用的不同的属性名;
this.name11 = name;
this.age11 = age;
this.sub11 = sub;
}
}

const person = new Person("fsllala", 18, false);

class

需要在constructor上面先声明

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {
name11:string
age11:number
sub11:boolean
constructor(name: string, age: number, sub: boolean) {
//这里为了更好的区分,采用的不同的属性名;
this.name11 = name;
this.age11 = age;
this.sub11 = sub;
}
}

const person = new Person("fsllala", 18, false);

类的修饰符

总共有三个:public private protected

  • public(class默认):class定义的属性或方法内部外部都能访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person {
public name11: string
age11: number
sub11: boolean
constructor(name: string, age: number, sub: boolean) {
//这里为了更好的区分,采用的不同的属性名;
this.name11 = name;
this.age11 = age;
this.sub11 = sub;
}
}

const person = new Person("fsllala", 18, false);
console.log(person.name11); // fsllala
console.log(person.age11); // 18
  • private:只能在内部访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person {
public name11: string
private age11: number
sub11: boolean
constructor(name: string, age: number, sub: boolean) {
//这里为了更好的区分,采用的不同的属性名;
this.name11 = name;
this.age11 = age;
this.sub11 = sub;
}
}

const person = new Person("fsllala", 18, false);
console.log(person.name11); // fsllala
console.log(person.age11); // 这里会报错,提示: 属性“age11”为私有属性,只能在类“Person”中访问
  • protected:只能在内部和继承的子类中访问 不能在外部访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person {
public name11: string
private age11: number
protected sub11: boolean
constructor(name: string, age: number, sub: boolean) {
//这里为了更好的区分,采用的不同的属性名;
this.name11 = name;
this.age11 = age;
this.sub11 = sub;
}
}

class Man extends Person{
constructor(){
super("fsllala", 18, false);
console.log(this.name11);
console.log(this.age11); // 这里会报错,提示: 属性“age11”为私有属性,只能在类“Person”中访问
console.log(this.sub11); // 这里没问题,因为是继承的子类

}
}

static 静态属性 和 静态方法

静态方法中的this指向类本身,非静态方法的this指向实例对象;

即:静态方法中的this,只能调用static定义的属性或方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person {
public name11: string
private age11: number
protected sub11: boolean
static aaa:string="静态属性直接赋值";
constructor(name: string, age: number, sub: boolean) {
//这里为了更好的区分,采用的不同的属性名;
this.name11 = name;
this.age11 = age;
this.sub11 = sub;
}
static run():string{
// this.name11; //这里会报错,提示: 类型“typeof Person”上不存在属性“name11”。
this.aaa; //这里没问题,静态方法中的this指向对象本身
return "静态方法中的this指向对象本身"
}
}

console.log(Person.aaa);// 静态属性直接赋值
console.log(Person.run());// 静态方法中的this指向对象本身

interface 定义类

TS interface 定义类,使用关键字 implements ;后面跟interface的名字,多个用逗号隔开;继承还是用extends;

与C#或Java里接口的基本作用一样,TypeScript也能够用它来明确的强制一个类去符合某种契约。

extends可以接口继承接口,可以类继承类;implements是类实现接口;

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
interface PersonClass {
get(type: boolean): boolean
}

interface PersonClass2 {
set(): void,
asd: string
}

class A {
name: string
constructor() {
this.name = "123"
}
}

class Person extends A implements PersonClass, PersonClass2 {
asd: string
constructor() {
super()
this.asd = '123'
}
get(type: boolean) {
return type
}
set() {

}
}

抽象类

应用场景:如果你写的类实例化之后毫无用处此时我可以把他定义为抽象类

或者你也可以把他作为一个基类-> 通过继承一个派生类去实现基类的一些方法

  • 下面这段代码会报错,因为抽象类无法被实例化
1
2
3
4
5
6
abstract class A {
public name:string

}

new A()
  • 我们在A类定义了 getName 抽象方法但未实现;
  • 我们B类实现了A定义的抽象方法 如果实现就不报错 我们定义的抽象方法必须在派生类实现
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
abstract class A {
name: string
constructor(name: string) {
this.name = name;
}
//没有 abstract 的可以直接定义方法
setName(name: string) {
this.name = name;
}
// 方法“getName”不能具有实现,因为它标记为抽象。
abstract getName(): string
}

class B extends A {
constructor() {
super('lala')
}
getName(): string {
return this.name
}
}

let b = new B();
b.setName("haha~")
console.log(b.getName()); // haha~

元组类型

  • 元组就是数组的变种;
  • 如果需要一个固定大小的不同类型值的集合,我们需要使用元组;
  • 元组(Tuple)是固定数量的不同类型的元素的组合。
  • 元组与集合的不同之处在于,元组中的元素类型可以是不同的,而且数量固定。元组的好处在于可以把多个元素作为一个单元传递。如果一个方法需要返回多个值,可以把这多个值作为元组返回,而不需要创建额外的类来表示。
1
2
3
let arr: [number, string] = [1, 'string']

let arr2: readonly [number, boolean, string, undefined] = [1, true, 'sring', undefined]

当赋值或访问一个已知索引的元素时,会得到正确的类型:

1
2
3
4
5
let arr:[number,string] = [1,'string']
arr[0].length // 这里会报错,提示:类型“number”上不存在属性“length”
arr[1].length // 这里 么的问题

//数字是没有length 的

越界元素

1
2
3
4
// 对于越界的元素他的类型被限制为 联合类型(就是你在元组中定义的类型)==>  string | number[]
let arr:[number,string] = [1,'string'];
arr.push(boolean); // 这里会报错,提示: “boolean”仅表示类型,但在此处却作为值使用。
arr.push(1);// 这里 么的问题

应用场景 例如定义excel返回的数据

1
2
3
4
5
6
7
let excel: [string, string, number, string][] = [
['title', 'name', 1, '123'],
['title', 'name', 1, '123'],
['title', 'name', 1, '123'],
['title', 'name', 1, '123'],
['title', 'name', 1, '123'],
];

枚举类型

  • 在JavaScript中是没有枚举的概念的TS帮我们定义了枚举这个类型;
  • 使用枚举:通过enum关键字定义我们的枚举;
  • 作用:定义常量,约束统一;

数字枚举

例如 红绿蓝 Red = 0 Green = 1 Blue= 2 分别代表红色0 绿色为1 蓝色为2

1
2
3
4
5
6
7
8
9
enum Color {
Red,
Green,
Blue,
}

console.log(Color.Red); //0
console.log(Color.Green); //1
console.log(Color.Blue); //2

这样写就可以实现应为ts定义的枚举中的每一个组员默认都是从0开始的所以也就是

1
2
3
4
5
6
7
8
9
10
enum Color {
Red = 0,
Green = 1,
Blue = 2,
}

console.log(Color.Red); //0
console.log(Color.Green); //1
console.log(Color.Blue); //2
//默认就是从0开始的 可以不写值

增长枚举

1
2
3
4
5
6
7
8
9
enum Color {
Red = 5,
Green,
Blue,
}

console.log(Color.Red); //5
console.log(Color.Green); //6
console.log(Color.Blue); //7

自定义枚举

1
2
3
4
5
6
7
8
9
enum Color {
Red = 5,
Green = 13,
Blue = 18,
}

console.log(Color.Red); //5
console.log(Color.Green); //13
console.log(Color.Blue); //18

字符串枚举

1
2
3
4
5
6
7
8
9
enum Color{
Red = 'red',
Green = 'green',
Blue = 'blue'
}

console.log(Color.Red); //red
console.log(Color.Green); //green
console.log(Color.Blue); //blue

异构枚举

枚举可以混合字符串和数字成员

1
2
3
4
enum Color{
No = "No",
Yes = 1,
}

接口枚举

定义一个枚举Color 定义一个接口AA 他有一个属性red 值为Color.Yes

声明对象的时候要遵循这个规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum Color {
No = "No",
Yes = 1,
}

interface AA {
red: Color.Yes
}

let obj1: AA = {
// 下面两种写法都可以
// red: Color.Yes
red: 1
}

const枚举

大多数情况下,枚举是十分有效的方案。 然而在某些情况下需求很严格。 为了避免在额外生成的代码上的开销和额外的非直接的对枚举成员的访问,我们可以使用 const枚举。 常量枚举通过在枚举上使用 const修饰符来定义

  • let 和 var 都是不允许声明enum的,只能使用const声明
  • const 声明的枚举会被编译成常量
  • 普通声明的枚举编译完后是个对象
  1. 普通声明编译之后
1
2
3
4
5
6
7
8
9
enum Color {
Yes,
No,
}

let code: number = 0;
if (code === Color.No) {

}

JS:

1
2
3
4
5
6
7
8
var Color;
(function (Color) {
Color[Color["Yes"] = 0] = "Yes";
Color[Color["No"] = 1] = "No";
})(Color || (Color = {}));
var code = 0;
if (code === Color.No) {
}
  1. const声明编译之后
1
2
3
4
5
6
7
8
9
const enum Color {
Yes,
No,
}

let code: number = 0;
if (code === Color.No) {

}

JS:

1
2
3
var code = 0;
if (code === 1 /* Color.No */) {
}

反向映射

  • 它包含了正向映射( name -> value)和反向映射( value -> name
  • 要注意的是 不会为字符串枚举成员生成反向映射。
1
2
3
4
5
6
7
enum Enum {
success
}
let result: number = Enum.success;
console.log(result); //0
let result2 = Enum[result];
console.log(result2); //success

类型推论|类型别名

类型推论

我声明了一个变量但是没有定义类型

TypeScript 会在没有明确的指定类型的时候推测出一个类型,这就是类型推论

类型推论

1
2
3
let str = "fsllala";
// 会报错,虽然我们没用明确限制类型,但是TS编辑器会自动推论为string类型。就不能够在赋值给别的类型
str = 18; //这里会报错,提示: 不能将类型“number”分配给类型“string”。

如果你声明变量没有定义类型也没有赋值这时候 TS 会推断成 any 类型可以进行任何操作

1
2
3
4
5
let str//为any类型
str = 123
str = "马杀鸡"
str = false
str = []

类型别名

type 关键字(可以给一个类型定义一个名字)多用于复合类型

  • 定义类型别名
1
2
3
4
type s = string;

let str: s = "fsllala";
console.log(str); // fsllala
  • 联合类型:指定多种类型,在前文有提到;
1
2
3
type s = string|number
let str:s = "永恒的紫罗兰花园"
let num:s = 520//这有这两种类型可以
  • 定义函数别名
1
2
3
4
type s = () => string

let str: s = () => "fsllala";
console.log(str()); //fsllala
  • 定义值的别名
1
2
3
4
5
type value = boolean | 0 | '213'

//变量的值 只能是上面value定义的值
let s: value = "213";
let s2: value = "111"; //这里会报错,提示: 不能将类型“"111"”分配给类型“value”。

never类型

TypeScript 将使用 never 类型来表示不应该存在的状态,也就是值永不存在的类型。

  • 值会永不存在的两种情况:
    1. 如果一个函数执行时抛出了异常,那么这个函数永远不存在返回值(因为抛出异常会直接中断程序运行,这使得程序运行不到返回值那一步,即具有不可达的终点,也就永不存在返回了);
    2. 数中执行无限循环的代码(死循环),使得程序永远无法运行到函数返回值那一步,永不存在返回。
1
2
3
4
5
6
7
8
9
10
11
12
// 返回never的函数必须存在无法达到的终点

// 因为必定抛出异常,所以 error 将不会有返回值
function error(message: string): never {
throw new Error(message);
}

// 因为存在死循环,所以 loop 将不会有返回值
function loop(): never {
while (true) {
}
}

举一个我们可能会见到的例子:

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
interface Foo {
type: "保安"

}
interface Bar {
type: "草莓"
}

type All = Foo | Bar

function handleValue(val: All) {
switch (val.type) {
case '保安':
// 这里 val 被收窄为 Foo
break
case '草莓':
// val 在这里是 Bar
break
default:
//兜底逻辑 一般是不会进入这儿 如果进来了就是程序异常了
// val 在这里是 never
const exhaustiveCheck: never = val
break
}
}

handleValue({ type: "草莓" });

比如新来了一个同事他新增了一个C接口,我们必须手动找到所有 switch 代码并处理,否则将有可能引入 BUG 。

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
interface Foo {
type: "保安"

}
interface Bar {
type: "草莓"
}


interface C {
type: "卷心菜"
}

type All = Foo | Bar | C

function handleValue(val: All) {
switch (val.type) {
case '保安':
// 这里 val 被收窄为 Foo
break
case '草莓':
// val 在这里是 Bar
break
default:
//兜底逻辑 一般是不会进入这儿 如果进来了就是程序异常了
const exhaustiveCheck: never = val; // 这里会报错,会提示: 不能将类型“C”分配给类型“never”
break
}
}

handleValue({ type: "草莓" });

所以通过这个办法,你可以确保 handleValue 总是穷尽 (exhaust) 了所有 All 的可能类型。

symbol类型

ES6 引入了一种新的基本数据类型Symbol,表示独一无二的值;

内存地址的指针位置不同,所以是唯一值;

1
2
3
4
5
6
7
8
9
10
11
/**
* Symbol(params)为基本数据类型,所以不能new;
* params 可以不传,没啥实际意义,唯一作用是为了区分声明的symbol
* params 一般传 number或者string
*/
let name1: symbol = Symbol("fsllala");
let name2: symbol = Symbol("fsllala");
let age:symbol=Symbol(18);

//这个值看似一样,其实因为内存地址指针位置不同,所以是唯一值
console.log(name1 === name2); // false
  • 普通方法获取不到对象中的symbol属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let name1: symbol = Symbol("fsllala");
let name2: symbol = Symbol("fsllala");
let age: symbol = Symbol(18);
let obj1 = {
[name1]: "forward",
[age]: 24,
sex: 1,
height: 173,
}

for (let i in obj1) {
console.log(i); // //只会打印出sex跟height,[name1]与[age]将打印不出来
}

console.log(Object.keys(obj1)); // [ 'sex', 'height' ]

console.log(Object.getOwnPropertyNames(obj1)); // [ 'sex', 'height' ]
  • 获取对象中的symbol的属性的两种方式

静态方法 Reflect.ownKeys() 返回一个由目标对象自身的属性键组成的数组(Array)。

语法:Reflect.ownKeys(target) => target 获取自身属性键的目标对象。

Reflect.ownKeys 方法返回一个由目标对象自身的属性键组成的数组。它的返回值等同于 Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target) ) 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let name1: symbol = Symbol("fsllala");
let name2: symbol = Symbol("fsllala");
let age: symbol = Symbol(18);
let obj1 = {
[name1]: "forward",
[age]: 24,
sex: 1,
height: 173,
}

//能打印出来两个Symbol,另外两个普通的不会打印出来
console.log(Object.getOwnPropertySymbols(obj1)); // [ Symbol(fsllala), Symbol(18) ]

// Reflect.ownKeys() 此属性是将所有的属性都列出来
console.log(Reflect.ownKeys(obj1)); //[ 'sex', 'height', Symbol(fsllala), Symbol(18) ]

Symbol.iterator 迭代器 和 生成器 for of

  • 一般来说Object对象是不能通过for…of来进行遍历的;
1
2
3
4
5
6
7
8
let obj1 = {
name:"fsllala",
age:18,
sex:1
}

// 这里会报错
for(let i of obj1); // 类型“{ name: string; age: number; sex: number; }”必须具有返回迭代器的 "[Symbol.iterator]()" 方法。
  • 可以给Object对象提供一个Iterator方法,使对象也能使用for…of遍历;
  • 迭代器:Symbol.iterator

要知道js的迭代器,我们就要先知道迭代的意思,实际上迭代的本意是遍历,随着不断有新的数据类型加入,我们就要用不同的for语句去循环,这就显得很麻烦,所以就有了迭代的概念

  • 迭代

所有数据类型的遍历都遵循的一个协议。所以以后你对遵循了迭代协议的数据类型 你只需要用迭代去遍历就可以了。顺便一提:for…of是用迭代器来实现的,只要你能用for…of来遍历的数据都是实现了迭代协议的数据

  • 实现的迭代协议的数据类型:

string,array,map,set,arguments,Nodelist等dom元素 ;注意注意;没有object

  • 生成器: for…of

for…of 会遍历可迭代的对象,调用对象上的 Symbol.iterator 方法。(此对象非彼对象,这个对 象是指你即将下手的目标)

对象也是不支持的,因为对象没用 Symbol.iterator 方法。

迭代器

迭代器Iterator 的用法

  1. Iterator 是 es6 引入的一种新的遍历机制 ,两个核心:

    1. 迭代器是一个统一的接口,它的作用是使各种 数据结构 可被便捷的访问,它是通过一个键为 Symbol.iterator 的方法来实现。
    2. 迭代器是用于 遍历 数据结构元素的指针(如数据库中的游标)。
  2. 使用迭代:

    1. 实现的迭代协议的数据类型,里面都会有一个 [Symbol.iterator]方法
    2. 进行迭代的时候用迭代器的next来取值,这个next返回的是一个对象:{value:value,done:"是否完成遍历"}
    3. 当 value是underfined 、done 为 true 时则遍历结束
  3. 实现的迭代协议的数据类型:

    string、array、map、set、arguments、Nodelist等dom元素;注意注意;没有object

1
2
3
4
5
6
7
8
9
const str: string = "fsl";
const iterator: Iterator<string> = str[Symbol.iterator]();

//next一次只遍历一个数,下一次调用将从上一次遍历到的位置开始下一个
console.log(iterator.next()); // { value: 'f', done: false }
console.log(iterator.next()); // { value: 's', done: false }
console.log(iterator.next()); // { value: 'l', done: false }
console.log(iterator.next()); // { value: undefined, done: true }
//返回的有两个属性,一个value,一个done。value当读取到值的时候,done为false、读取不到为true
1
2
3
4
5
6
7
8
9
let arr: Array<number> = [1, 5, 6]; // 泛型
let iterator: Iterator<number> = arr[Symbol.iterator]()//注意这里的接收类型<number>是固定要写的

//next一次只遍历一个数,下一次调用将从上一次遍历到的位置开始下一个
console.log(iterator.next()); //{ value: 1, done: false }
console.log(iterator.next()); //{ value: 5, done: false }
console.log(iterator.next()); //{ value: 6, done: false }
console.log(iterator.next()); //{ value: undefined, done: true }
//返回的有两个属性,一个value,一个done。value当读取到值的时候,done为false、读取不到为true
  • 实现一个简易的迭代器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let arr: Array<number | string> = ["泛型", "实现简单的迭代器"];
let set: Set<number> = new Set([1, 2, 3, 4]);
function iteratorFun(args: any): void { //这里定义为any类型是因为上面要传到这里的有多种不同类型
const it: Iterator<any> = args[Symbol.iterator]();
let next: any = { done: false };
while (!next.done) { //判断next,由于next默认为fasle,while循环只有true会通过,所以需要 取反
next = it.next(); //刚开始是声明next给个默认值,等到开始循环的时候再把真正的值赋给他
if (!next.done) {
console.log(next);
}
}
}

iteratorFun(arr);
iteratorFun(set);

泛型

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候在指定类型的一种特性

函数泛型

我写了两个函数一个是数字类型的函数,另一个是字符串类型的函数,其实就是类型不同,实现的功能是一样的,这时候我们就可以使用泛型来优化。

1
2
3
4
5
6
7
8
9
10
function num(a: number, b: number): Array<number> {
return [a, b];
}

function str(a: string, b: string): Array<string> {
return [a, b];
}

console.log(num(1, 2)); // [ 1, 2 ]
console.log(str('fsllala', '18')); // [ 'fsllala', '18' ]
  • 泛型优化

语法为函数名字后面跟一个<参数名> 参数名可以随便写 例如我这儿写了T;

当我们使用这个函数的时候把参数的类型传进去就可以了 (也就是动态类型);

即:定义这个函数的时候,不定义参数的类型,而是让调用者告知我这里的参数是什么类型;

1
2
3
4
5
6
7
8
9
10
function add<T>(a: T, b: T): Array<T> {
return [a, b];
}

add<number>(1,2);
add<string>("fsllala",'18');

//甚至我们可以简写
add(1,2)
add('1','2')//编辑器会自动推断类型,但最好还是写一下,如果你知道你具体需要的是什么的话

我们也可以使用不同的泛型参数名,只要在数量上和使用方式上能对应上就可以

1
2
3
4
5
6
function sub<T, U>(a: T, b: U): Array<T | U> {
let arr: Array<T | U> = [a, b];
return arr;
}

sub<number,boolean>(66,true);

泛型接口

声明接口的时候 在名字后面加一个 <参数>

使用的时候传递类型

  • 使用含有泛型的接口来定义对象的形状:
1
2
3
4
5
6
7
8
9
10
11
interface IKun<T> {
name: T,
age: number,
hobby: T[]
}

const kunkun: IKun<string> = {
name: "lala",
age: 18,
hobby: ["唱", "跳", "rap", "篮球"]
}
  • 泛型接口默认值
1
2
3
4
5
6
7
8
9
10
11
12
interface IKun<T = string> {
name: T,
age: number,
hobby: T[]
}

// 这里不写类型就会走默认值
const kunkun: IKun = {
name: "lala",
age: 18,
hobby: ["唱", "跳", "rap", "篮球"]
}
  • 使用含有泛型的接口来定义函数的形状:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface CreateArrayFunc<T> {
(length: number, value: T): Array<T>
}

let createArrayFunc: CreateArrayFunc<string>;

createArrayFunc = function <T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}

let results = createArrayFunc(3, "x");
console.log(results); // [ 'x', 'x', 'x' ]

泛型类

1
2
3
4
5
6
7
8
9
10
class Point<T>{
x: T
y: T
constructor(x: T, y: T) {
this.x = x;
this.y = y;
}
}
const point1 = new Point<number>(11, 22);
const point2 = new Point<string>('11', '22');

泛型约束

1
2
3
4
// 下面代码是会报错的
function getLength<T>(args:T){
return args.length; //这里会报错,提示: 类型“T”上不存在属性“length”。
}
  • 使用extends关键字添加泛型约束;
1
2
3
4
5
6
7
8
9
10
11
12
interface Ilength {
length: number
}
function getLength<T extends Ilength>(args: T) {
return args.length;
}

// 只要传入的实参有length属性,且值是number类型的就行;
// 即:有number类型的length就实现了接口Len
getLength("123");
getLength([1, 2]);
getLength({ length: 77 });
  • 使用keyof 约束对象

    平时在开发中我们可能会看到一些常用的名称:

    • T:Type的缩写,类型;
    • K、V:key和value的缩写,键值对;
    • E:Element的缩写,元素;
    • O:Object的缩写,对象;

下面场景不会报错:

1
2
3
4
5
6
7
function prop<O>(obj: O, keys) {
return obj[keys];
}

let info = { a: 1, b: 2, c: 3 };
prop(info, "a");
console.log(prop(info, "d")); // undefined 现在没有 o["d"];但是不报错;

解决方法:其中使用了TS泛型和泛型约束。使用extends关键字添加泛型约束,keyof操作符获取对象键的联合类型;

  • keyof 操作符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface IKun {
name: string,
age: number
}

type IKunKeys = keyof IKun;
/**
* keyof 操作符获取 IKun 接口的所有键,它的返回类型是联合类型;以上写法等同于下面这个
* type IKunKeys = "name"|"age" ;
*/

const kunkun: IKunKeys = "name";
const kunkun2: IKunKeys = "age";
const kunkun3: IKunKeys = "sex"; // 不能将类型“"sex"”分配给类型“keyof IKun”

如上问题的解决:

1
2
3
4
5
6
7
8
// 用keyof 分隔 O,分隔完之后,就是联合类型: "a" | "b" | "c",最后用K继承联合类型,即K就是联合类型:"a" | "b" | "c"
function prop<O, K extends keyof O>(obj: O, keys: K) {
return obj[keys];
}

let info = { a: 1, b: 2, c: 3 };
prop(info, "a");
console.log(prop(info, "d")); // 现在报错了,提示: 类型“"d"”的参数不能赋给类型“"a" | "b" | "c"”的参数。

参考文献

TS系列篇|泛型

模块化

先来看一个现象:不同TS模块下会产生命名冲突:

1
2
3
// index.ts
const name1 = "fsllala123";
console.log(name1);
1
2
3
// index2.ts
const name1="fsllala456";
console.log(name1);

模块化

什么是模块化?

  • JavaScript 规范声明任何没有 export 的 JavaScript 文件都应该被认为是一个脚本,而非一个模块。在一个脚本文件中,变量和类型会被声明在共享的全局作用域。
  • 如果你有一个文件,现在没有任何 import 或者 export,但是你希望它被作为模块处理,添加这行代码; 这会把文件改成一个没有导出任何内容的模块,这个语法可以生效,无论你的模块目标是什么。
1
export{};

ES Module

TypeScript中最主要使用的模块化方案就是ES Module,也就是ES6的写法,详见文章模块化规范

TS文件中,即使两个不同的TS文件,比如index.tsindex2.ts,命名同一个变量,也会起冲突。因为它的内容被视为全局可见的(因此对模块也是可见的)

TypeScript与ECMAScript 2015一样,任何包含顶级import或者export的文件都被当成一个模块。相反地,如果一个文件不带有顶级的import或者export声明,那么它的内容被视为全局可见的(因此对模块也是可见的)

ES6模块化

  • 解决全局变量冲突(每个文件添加export{}即可)
1
2
3
4
// index.ts
const name1 = "fsllala123";
console.log(name1);// fsllala123
export{}
1
2
3
4
// index2.ts
const name1="fsllala456";
console.log(name1); // fsllala456
export{}
  • 模块化引用
1
2
3
// index.ts
const name1 = "fsllala123";
export { name1 }
1
2
3
// index2.ts
import { name1 } from "./index"
console.log(name1); // fsllala123

内置类型导入

1
2
3
4
5
6
7
8
// type.ts 声明TS类型
export interface IKun {
name: string,
age: number,
hobby: string[]
}

export type IPerson = string | number;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// index.js 使用

// 这两种写法都可以 使用 type 前缀,表明被导入的是一个类型;
// import { type IKun, type IPerson } from "./type";
import type { IKun, IPerson } from "./type"; // 相当于把type提出来了

const person1: IPerson = "fsllala";

const kunkun: IKun = {
name: "xiaoheizi",
age: 18,
hobby: ["唱", "跳", "rap", "篮球"]
}

console.log(kunkun);

这里需要注意的是:不使用type前缀,也可以导入类型。那么为啥要加上type前缀呢?

TS最终要被编译成JS代码,类型是没用的,通过语法识别TS/JS的成本要比直接引入的时候就用type表明了是类型的成本大,到时候类型部分的代码就直接删除了;

namespace命名空间

  • 不同TS模块下会产生命名冲突:

  • 我们在工作中无法避免全局变量造成的污染,TypeScript提供了namespace 避免这个问题出现

    • 内部模块,主要用于组织代码,避免命名冲突
    • 命名空间内的类默认私有
    • 通过 namespace 关键字定义,然后通过 export 暴露;(namespace里面如果不通过export暴露,那么外面访问不到)

namespace常规写法

  • namespace,里面不使用export:
1
2
3
4
5
6
7
//不能获取到里面的属性,在命名空间外访问是一个空对象
namespace AA {
const name1 = "fsllala";
const age1 = 18;
}

console.log(AA); // {}
  • namespace,里面使用export:
1
2
3
4
5
6
// 可以获取到里面的属性了
namespace AA {
export const name1 = "fsllala";
export const age1 = 18;
}
console.log(AA); // 是个对象 { name1: 'fsllala', age1: 18 }
  • 同一模块(不同模块)下有重名的namespace,里面属性会合并

因为属性会合并,所以会出现以下情况:

  • 重名namespace属性合并,情况一:
1
2
3
4
5
6
7
8
9
10
// 不同命名空间的属性名不冲突
namespace AA {
export const name1 = "fsllala";
}

namespace AA {
export const age1 = 18;
}

console.log(AA); // { name1: 'fsllala', age1: 18 }
  • 重名namespace属性合并,情况二:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 属性或方法使用const/let定义,会报错:
* 其实也不难理解:
* 命名空间一样的合并,即:在一个对象里面,const/let在一个对象里面会有产生块级作用域;
* const/let在同一作用域无法重新声明变量
*/

namespace AA {
export const name1 = "fsllala"; // 无法重新声明块范围变量“name1”。
export const age1 = 18; // 无法重新声明块范围变量“age1”。
}

namespace AA {
export const name1 = "fsllala"; // 无法重新声明块范围变量“name1”。
export const age1 = 18; // 无法重新声明块范围变量“age1”。
}
  • 重名namespace属性合并,情况三:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 属性或方法使用var定义,么的问题:
* 后者会覆盖前者
*/

namespace AA {
export var name1 = "fsllala";
export var age1 = 18;
}

namespace AA {
export var name1 = "fsllala";
export var age1 = 18;
}

console.log(AA); // { name1: 'fsllala', age1: 18 }

嵌套命名空间

1
2
3
4
5
6
7
8
9
// namespace里面套namespace(ps:同理里面的namespace如果没有export,外面访问不到)
namespace AA {
export namespace BB {
export const name1 = "fsllala";
export const age1 = 18;
}
}

console.log(AA); // 是个对象 { BB: { name1: 'fsllala', age1: 18 } }

抽离命名空间

1
2
3
4
5
6
7
8
// index.ts里导出文件
// 将最外层 namespace 通过 export导出
export namespace AA {
export namespace BB {
export const name1 = "fsllala";
export const age1 = 18;
}
}
1
2
3
// index2.ts里面导入文件
import { AA } from "./index"
console.log(AA); // { BB: { name1: 'fsllala', age1: 18 } }

简化命名空间

除了使用const还可以使用import来简化;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// index2.ts里面导入文件
import { AA } from "./index"


import { AA } from "./index"

// 感觉太长了
// console.log(AA.BB.name1); // fsllala

//method1:
const method1 = AA.BB.name1;
console.log(method1); //fsllala

//method2:
import method2 = AA.BB.name1;
console.log(`method2:${method2}`); // method2:fsllala

in操作符

如果指定的属性在指定的对象或其原型链中,则 in 运算符返回true;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface Fish {
swim(): void
}

interface Dog {
run: () => void
}


function move(animal: Fish | Dog) {
if ("swim" in animal) {
animal.swim(); // fish is swiming~~
} else {
animal.run();
}
}

move({
swim() {
console.log("fish is swiming~~");
}
})

三斜线指令

重点:我们知道在 TS 项目中,文件之间相互引用是通过 import来实现的,那么声明文件之间的引用呢?没错通过三斜线指令

三斜线指令常用的就两个功能:

  • 倒入声明文件
  • 倒入第三方包

如果对比 import 的功能,我们可以写出如下的等式:

  • /// <reference path="..." /> == import filename.xxx
  • /// <reference types="..." /> == import lodash from "lodash"

PS:下方代码使用ts-node输出有有问题,暂未找到解决方法;

But 通过tsc --outFile sample.js index.tsindex.ts编译成sample.js方法可行

1
2
3
4
5
6
7
8
9
10
11
12
13
//在a.ts
namespace A {
export const fn = () => 'a'
}
//在b.ts
namespace A {
export const fn2 = () => 'b'
}
//在index.ts
///<reference path="./index2.ts" />
///<reference path="./index3.ts" />
//引入之后直接可以使用变量 A
console.log(A);

声明文件

这里环境为:vite+vue3+ts;

  • 我们之前编写的typescript文件都是 .ts 文件,这些文件最终会输出 .js 文件,也是我们通常编写代码的地方;
  • 还有另外一种文件 .d.ts 文件,它是用来做类型的声明(declare),称之为类型声明(Type Declaration)或者类型定义(Type
    Definition)文件。
  • .d.ts文件是不需要导出的;让他直接在全局就ok;

外部定义类型声明 – 第三方库

有一些第三方库是没有写自己的声明文件的,我们在引用的时候,会报错。以lodash库为例(lodash是有第三方写好的声明文件库的,这里我们自己写)。

  1. 安装第三方库
1
npm i --save lodash
  1. 在vue文件中进入第三方库,由于没有声明文件,所以会报错。

  1. 手写声明文件

这里新建一个d.ts文件,例如 fsllala.d.ts

1
2
3
// fsllala.d.ts
// 根据 import _ from "lodash" 得知 lodash是从模块导入的; 所以声明一个模块,即: declare module
declare module "lodash";

然后在lodash.vue中发现报错消失了,即可以使用第三方库了(如果还报错,重启VS Code即可);

  1. 但是还不知道模块里面有什么东西,所以使用的时候不会有提示。可以在模块内部作用域自定义函数,解决不提示的问题。
1
2
3
4
5
6
//  fsllala.d.ts
// {}代表模块内部作用域,里面可以导出一些function;
declare module "lodash"{
export function max(...args:any[]):any;
export function mean(...args:any[]):any;
}
1
2
3
4
5
6
// lodash.vue 输入_.有相关的api提示;
<script setup lang="ts">
import _ from "lodash";
console.log(_.max([4, 2, 8, 6])); // 8
console.log(_.mean([4, 2, 8, 6])); // 5
</script>

自己代码类型声明

  • 一般来说TS的类型是不需要单独抽离到d.ts文件中的。但是一些全局声明的变量/函数/类例外。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// vue中的 index.html中
// 变量
const whyName = "Fsllala";
const whyAge = 18;
const whyHeight = 188;
// 函数
function globalFunc(args) {
return `this is globalFunction~${args}`;
}
// 类
function Person(name, age) {
this.name = name;
this.age = age;
}

声明文件

上面如果是JS,就可以直接引用了(把script标签中的 “lang=’ts’”去掉,就可以用了);显然是ts的问题;

  • 新建一个d.ts的声明文件
1
2
3
4
5
6
7
8
9
10
11
12
// 为自己的全局  变量/函数/类 定义类型声明
declare const whyName: string;
declare const whyAge: number;
declare const whyHeight: number;

declare function globalFunc(args: string): string;

declare class Person {
name: string
age: number
constructor(name: string, age: number)
}

然后在lodash.vue中发现报错消失了,即可以使用全局变量/函数/类了(如果还报错,重启VS Code即可);

Partial

用于构造一个Type下面的所有属性都设置为可选的类型;

1
2
3
4
5
6
7
8
// 写法: Partial<Type>
interface IKun{
name:string,
age:number,
hobby:string[]
}

type IKunOptional = Partial<IKun>;

Partial

Record<Keys, Type>

用于构造一个对象类型,它所有的key(键)都是Keys类型,它所有的value(值)都是Type类型。

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
interface IKun {
name: string,
age: number,
hobby?: string[]
}

type t1 = "上海" | "北京" | "洛杉矶";

type IKuns = Record<t1, IKun>;

/**
* 鼠标放到 IKuns 会显示:
* type IKuns = {
上海: IKun;
北京: IKun;
洛杉矶: IKun;
}
* 所以说 上海/北京/洛杉矶 一个都不能少
*/

// 上海/北京/洛杉矶 一个都不能少
const ikuns: IKuns = {
"上海": {
name: "xxx",
age: 18
},
"北京": {
name: "xxx",
age: 18
},
"洛杉矶": {
name: "xxx",
age: 18
},
}