TypeScript笔记(一)

TypeScript笔记(一)
Tom1.TypeScript的环境搭
一.什么是TypeScript
TypeScript是Javascript的超集,遵循最新的ES5/ES6规范。Typescript扩展了Javascript语法。
- Typescript更像后端JAVA,让
JS可以开发大型企业应用 - TS提供的类型系统可以帮助我们在写代码时提供丰富的语法提示
- 在编写代码时会对代码进行类型检查从而避免很多线上错误
TypeScript不会取代JS, 尤雨溪: 我认为将类型添加到JS本身是一个漫长的过程 。让委员会设计一个类型系统是(根据TC39的经历来判断)不切实际的 。
二.环境配置
1.全局编译TS文件
全局安装typescript对TS进行编译
1 | npm install typescript -g |
1 | tsc # 可以将ts文件编译成js文件 |
2.配置 webpack 环境
安装依赖
1
npm install rollup typescript rollup-plugin-typescript2 @rollup/plugin-node-resolve rollup-plugin-serve -D
初始化
TS配置文件1
npx tsc --init
webpack配置操作rollup.config.js文件
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// 默认rollup 打包的时候会查找当前目录下 rollup.config.js这个文件
// 采用es模块来编写配置文件
// node中有模块规范默认是 commonjs , 也可以改成esm模块规范
import ts from 'rollup-plugin-typescript2'
import { nodeResolve } from '@rollup/plugin-node-resolve'
import path from 'path'
import { fileURLToPath } from "url"
// 当前文件的绝对路径 file://xxxx/xxx/xxx
const __filename = fileURLToPath(import.meta.url); // 当前文件的绝对路径
const __dirname = path.dirname(__filename); // 当前文件所在的文件夹目录 绝对路径
// 打包的配置对象
export default {
input: './src/index.ts', // 项目入口
output: {
file: path.resolve(__dirname, 'dist/bundle.js'), // 当前的文件在当前目录下的dist目录
format: 'iife', // (function(){})()
sourcemap:true
},
plugins: [
nodeResolve({
extensions: ['.js', '.ts']
}), // (第三方包的入口)入口文件可以是js 也可以是ts
ts({
tsconfig: path.resolve(__dirname, 'tsconfig.json')
})
]
}package.json配置1
2
3
4"scripts": {
"dev": "rollup -c -w"
}我们可以通过
npm run start启动服务来使用typescript啦~package.json文件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23{
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.1.0",
"@types/node": "^20.5.3",
"rollup": "^3.27.2",
"rollup-plugin-typescript2": "^0.35.0",
"typescript": "^5.1.6"
},
"type": "module",
"scripts": {
"dev": "rollup -c -w"
},
"dependencies": {
"@types/jquery": "^3.5.17",
"@types/react": "^18.2.21",
"axios": "^1.4.0",
"jquery": "^3.7.0",
"mitt": "^3.0.1",
"reflect-metadata": "^0.1.13",
"vue": "^3.3.4"
}
}我门在使用ts的时候 需要将编写的ts代码转换成js 在运行
typescript这个模块 来进行文件的编译
npm install typescript -g 全局的包只能在命令行中使用 tsc
最终直接生成js 文件在运行。
tsc —init 初始化ts的配置文件
比较适合临时测试的方式
vscode插件来实现代码的运行
code-runner 如果是js文件 内部会直接采用 node + 文件名来执行此文件 ,如果是ts文件 需要通过ts-node 来直接执行
sudo npm install ts-node -g
通过构建工具将代码转化成js 在去运行 (webpack,rollup,esbuild) 最终便衣成js执行
2.基础类型
TS中冒号后面的都为类型标识
布尔类型 boolean
布尔值 最基本的数据类型就是简单的true/false值,在JavaScript和TypeScript里叫做boolean(其它语言中也一样)
1 | let bool:boolean = true; |
数字类型 number
数字 和JavaScript一样,TypeScript里的所有数字都是浮点数。 这些浮点数的类型是 number。 除了支持十进制和十六进制字面量,TypeScript还支持ECMAScript 2015中引入的二进制和八进制字面量。
1 | let num:number = 10; |
字符串类型 string
字符串 JavaScript程序的另一项基本操作是处理网页或服务器端的文本数据。 像其它语言里一样,我们使用string表示文本数据类型。 和JavaScript一样,可以使用双引号(")或单引号(')表示字符串
1 | let str:string = 'hello'; |
你还可以使用模版字符串,它可以定义多行文本和内嵌表达式。 这种字符串是被反引号包围(``),并且以${ expr }` 这种形式嵌入表达式
1 | let name: string = `Gene`; |
这与下面定义sentence的方式效果相同:
1 | let sentence: string = "Hello, my name is " + name + ".\n\n" +"I'll be " + (age + 1) + " years old next month."; |
数组 array
TypeScript像JavaScript一样可以操作数组元素。 有两种方式可以定义数组。 第一种,可以在元素类型后面接上[],表示由此类型元素组成的一个数组
1 | let list: number[] = [1, 2, 3]; |
第二种方式是使用数组泛型,Array<元素类型>:
1 | let list: Array<number> = [1, 2, 3]; |
元组类型 Tuple
元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。 比如,你可以定义一对值分别为string和number类型的元组。
1 |
|
1 | let tuple:[string,number,boolean] = ['zf',10,true]; |
当访问一个已知索引的元素,会得到正确的类型
1 | console.log(x[0].substr(1)); // OK |
当访问一个越界的元素,会使用联合类型替代
1 | x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型 |
你赋予的值要求得符合这个结构和顺序, 元组在新增内容的时候 不能增加额外的类型的值,只能是已有的,而且增加后无法访问
1 | let tuple: [string, number, string, number] = ['1', 1, '1', 1] |
已经约定好没有第四个,后续增加的不算,访问的时候不能访问后增加,安全问题
1 | let item: string = tuple[2] |
3.枚举类型 enum
enum类型是对JavaScript标准数据类型的一个补充。 像C#等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字 常用于 状态码 权限 数据格式 标志位
1 | enum Color {Red, Green, Blue} |
默认情况下,从0开始为元素编号。 你也可以手动的指定成员的数值。 例如,我们将上面的例子改成从1开始编号:
1 | enum Color {Red = 1, Green, Blue} |
或者,全部都采用手动赋值:
1 | enum Color {Red = 1, Green = 2, Blue = 4} |
枚举类型提供的一个便利是你可以由枚举的值得到它的名字。 例如,我们知道数值为2,但是不确定它映射到Color里的哪个名字,我们可以查找相应的名字:
1 | enum Color {Red = 1, Green, Blue} |
1 | enum USER_ROLE { |
可以枚举,也可以反举
1 | // 编译后的结果 |
异构枚举
1
2
3
4
5enum USER_ROLE {
USER = 'user',
ADMIN = 1,
MANAGER,
}常量枚举
1
2
3
4
5
6const enum USER_ROLE {
USER,
ADMIN,
MANAGER,
}
console.log(USER_ROLE.USER)// console.log(0 /* USER */);
任意值 any
有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。 这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么我们可以使用any类型来标记这些变量:
any 任何类型 能不写any 就不要用any , any会导致类型丧失检测 anyScript(如果我们的项目是采用ts编写的,一般情况下any的出现场景不多) 放弃检测,出错就怨自己。 没有ts的加持
1 | let notSure: any = 4; |
在对现有代码进行改写的时候,any类型是十分有用的,它允许你在编译时可选择地包含或移除类型检查。 你可能认为Object有相似的作用,就像它在其它语言中那样。 但是Object类型的变量只是允许你给它赋任意值 - 但是却不能够在它上面调用任意的方法,即便它真的有这些方法:
1 | let notSure: any = 4; |
当你只知道一部分数据的类型时,any类型也是有用的。 比如,你有一个数组,它包含了不同的类型的数据:
1 | let list: any[] = [1, true, "free"]; |
空值 void
某种程度上来说,void类型像是与any类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是void:
只能接受null,undefined。一般用于函数的返回值
严格模式下不能将
null赋予给voidvoid 类型代表的是空类型 undefiend void 这个void一般值表示函数的返回值
unefiend 可以赋予给void, 都代表空 (undefiend 是 void的子类型)
1 | function warnUser(): void { |
声明一个void类型的变量没有什么大用,因为你只能为它赋予undefined和null:
1 | let unusable: void = undefined; |
Null 和 Undefined
TypeScript里,undefined和null两者各自有自己的类型分别叫做undefined和null。 和void相似,它们的本身的类型用处不是很大:
1 | // Not much else we can assign to these variables! |
默认情况下null和undefined是所有类型的子类型。 就是说你可以把null和undefined赋值给number类型的变量。
如果禁用非严格null检测,null和undefiend 可以赋予给任何类型 (null,undefiend任何类型的子类型)
如果
strictNullChecks的值为true,则不能把null 和 undefined付给其他类型
然而,当你指定了--strictNullChecks标记,null和undefined只能赋值给void和它们各自。 这能避免很多常见的问题。 也许在某处你想传入一个string或null或undefined,你可以使用联合类型string | null | undefined。 再次说明,稍后我们会介绍联合类型。
注意:我们鼓励尽可能地使用
--strictNullChecks,但在本手册里我们假设这个标记是关闭的。
Never
never类型表示的是那些永不存在的值的类型。 例如,never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型; 变量也可能是never类型,当它们被永不为真的类型保护所约束时。
never类型是任何类型的子类型,也可以赋值给任何类型;
never代表不会出现的值。不能把其他类型赋值给never
然而,没有类型是never的子类型或可以赋值给never类型(除了never本身之外)。 即使any也不可以赋值给never。
下面是一些返回never类型的函数:
1 | // 返回never的函数必须存在无法达到的终点 |
never类型 只能被never类型来赋予值
Symbol类型
Symbol表示独一无二
1 | const s1 = Symbol('key'); |
BigInt类型
number类型和bigInt类型是不兼容的
1 | const num1 = Number.MAX_SAFE_INTEGER + 1; |
object对象类型
1 | let create = (obj:object):void=>{} |
练习代码
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// ts关注的是类型,不是业务逻辑)
// 类型的分类: 基础类型、高级类型、内置类型、自定义类型、类型体操
// TS的类型都是在变量后面来写 :后面是类型 = 后面是值 (ts语法,不是js对象)
// ts的特点,如何来学习?
// 1).ts的目的是什么?从安全角度来考虑使用 (考虑我在赋予结果的时候 是否会发生错误) error lens
// 2).ts是用来检测类型的,只是提示作用,不是在运行的时候发生的 (运行的时候和ts无关,“代码没有被执行”)
// 3).编译ts之后 类型就消失了,不存在类型了(写的都是空气) 最终生生产环境下 可以增添.d.ts 来对js文件增加类型声明
// ts的特点:在编写代码的时候 并不是所有的变量都要添加类型。 (ts中支持类型推导,根据赋的值来猜测他的类型) 如果猜测的类型是对的不用给类型, 如果猜测的不对,或者类型无法正确的推导 自己写类型
// 1) string number boolean
const name: string = 'jw';
const age: number = 30;
const gender: boolean = true;
// 基础类型, 包装类型 规范 小写的类型一般用于描述基本类型 大写的用来描述的是实例类型
let s1: string = 'abc';
// let s2:string = new String('abc')
let s3: String = '1'; // 在赋予值的时候 子集可以赋予给父级
let s4: String = new String('1'); // 类的类型,类类型,用来描述实例的
// 我们在使用基本类型的时候 需要采用的时候 小写类型来标识
// 数组的概念:用于存储多个类型相同的集合
// 类型[] Array<类型> 都可以用于声明数组
let arr1: number[] = [1, 2, 3, 4, 5];
let arr2: Array<number> = [1, 2, 3, 4, 5];
let arr3: (number | string)[] = [1, 2, 3, 'a', 'b', 'c'];
// 数组要求的是存储的格式按照特定类型来存储,不关心位置
// 元组 tuple
// 你赋予的值要求得符合这个结构和顺序, 元组在新增内容的时候 不能增加额外的类型的值,只能是已有的,而且增加后无法访问
let tuple: [string, number, string, number] = ['1', 1, '1', 1]
// 已经约定好没有第四个,后续增加的不算,访问的时候不能访问后增加,安全问题
let item: string = tuple[2]
// 枚举: 自带类型的对象(自己有类型,就是一个对象)
// 约定一组格式我们会用枚举 状态码 权限 数据格式 标志位
// 维护一组常量的时候 可以采用枚举
const enum STATUS { // 常量枚举 不会额外编译成对象, 所以更节约
'OK' = 'ok',
'NO_OK' = 100,
'NOT_FOUND'
}
// 类型可以进行反举 (值是数字的时候 可以反过来枚举), 枚举没有值会根据上面的索引来自动累加
// 异构枚举 就是枚举中不光有数字 还有字符串. 异构枚举上一个是字符串下一个无法推导
const r = STATUS['OK'];
console.log(r)
// null undefined 基本类型 ,正常情况下只能赋予给null 和 undefined
const u: undefined = undefined
const n: null = null
// 如果禁用非严格null检测,null和undefiend 可以赋予给任何类型 (null,undefiend任何类型的子类型)
// void 类型代表的是空类型 undefiend void 这个void一般值表示函数的返回值
// unefiend 可以赋予给void, 都代表空 (undefiend 是 void的子类型)
function a(): void {
return undefined
}
// never 永远不,永远达到不了的地方就是never
// 函数无法执行完毕
function whileTrue(): never {
while (true) { }; // 函数无法达到执行完毕的状态
}
function throwError(): never {
throw Error(); // 出错函数无法执行完毕
}
// 如果if/else 条件都走完了, 没有遗漏的 后面的类型就是never (完整性保护)
// 111 [1,1,1]
// '111' ['1','1','1']
// true ['t','r','u','e']
function validateCheck(v:never){}
function toArray(val:number | string | boolean) {
if(typeof val === 'number'){
return val.toString().split('').map(Number) // [1,1,1]
}
if(typeof val === 'string'){
return val.split('')// [1,1,1]
}
if(typeof val === 'boolean'){
return val.toString().split('') // [1,1,1]
}
// never类型 只能被never类型来赋予值
validateCheck(val); // 代码的完整性保护
}
toArray('abc')
// ...
// any 任何类型 能不写any 就不要用any , any会导致类型丧失检测 anyScript(如果我们的项目是采用ts编写的,一般情况下any的出现场景不多) 放弃检测,出错就怨自己。 没有ts的加持
// let a1:any = 1;
// a1 = '1';
// a2 = function(){}
// object 引用类型
function create(val:object){ // {} object Object的区别
}
create({})
create(function(){})
create([])
const symbol:symbol = Symbol()..
const bigint:bigint = BigInt(Number.MAX_SAFE_INTEGER + 1)
// symbol bigInt
// 非严格模式在关闭的时候 null可以赋予给undefiend
// string number boolean 数组 元组 枚举 null undefiend void never any objectr symbol bigInt
export { } // 这是一个独立的模块,不好影响其他人
3.类型推导(type inference)
一. 类型推导
声明变量没有赋予值时默认变量是any类型
1 | let name; // 类型为any |
声明变量赋值时则以赋值类型为准
1 | let name = 'hello'; // name被推导为字符串类型 |
TypeScript里,在有些没有明确指出类型的地方,类型推论会帮助提供类型。如下面的例子
1 | let x = 3; // 类型被推导为 number |
变量
x的类型被推断为数字。 这种推断发生在初始化变量和成员,设置默认参数值和决定函数返回值时
1 | let x = [0, 1, null]; // 类型被推导为 number | null |
为了推断
x的类型,我们必须考虑所有元素的类型。 这里有两种选择:number和null。 计算通用类型算法会考虑所有的候选类型,并给出一个兼容所有候选类型的类型。
由于最终的通用类型取自候选类型,有些时候候选类型共享相同的通用类型,但是却没有一个类型能做为所有候选类型的类型。例如
1 | let zoo = [new Rhino(), new Elephant(), new Snake()]; // (Rhino | Elephant | Snake)[] |
这里,我们想让zoo被推断为
Animal[]类型,但是这个数组里没有对象是Animal类型的,因此不能推断出这个结果。 为了更正,当候选类型不能使用的时候我们需要明确的指出类型:
1 | let zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()]; |
const 是常量意味着定义的值不会修改所以他的类型是一个字面量类型 . const声明变量必须复制
let 声明变量 可以修改所以类型范围推到的结果会变大
1 | const a1 = 1 // const a1: 1 |
上下文类型
TypeScript类型推论也可能按照相反的方向进行。 这被叫做“按上下文归类”。按上下文归类会发生在表达式的类型与所处的位置相关时。比如:
1 | window.onmousedown = function(mouseEvent) { |
这个例子会得到一个类型错误,TypeScript类型检查器使用Window.onmousedown函数的类型来推断右边函数表达式的类型。 因此,就能推断出mouseEvent参数的类型了。 如果函数表达式不是在上下文类型的位置,mouseEvent参数的类型需要指定为any,这样也不会报错了。
如果上下文类型表达式包含了明确的类型信息,上下文的类型被忽略。 重写上面的例子:
1 | window.onmousedown = function(mouseEvent: any) { |
这个函数表达式有明确的参数类型注解,上下文类型被忽略。 这样的话就不报错了,因为这里不会使用到上下文类型。
上下文归类会在很多情况下使用到。 通常包含函数的参数,赋值表达式的右边,类型断言,对象成员和数组字面量和返回值语句。 上下文类型也会做为最佳通用类型的候选类型。比如:
1 | function createZoo(): Animal[] { |
这个例子里,最佳通用类型有4个候选者:Animal,Rhino,Elephant和Snake。 当然,Animal会被做为最佳通用类型。
二.包装对象
我们在使用基本数据类型时,调用基本数据类型上的方法,默认会将原始数据类型包装成对象类型
1 | let bool1:boolean = true; |
boolean是基本数据类型 , Boolean是他的封装类
三.联合类型(Union Types)
联合类型与交叉类型很有关联,但是使用上却完全不同。 偶尔你会遇到这种情况,一个代码库希望传入number或string类型的参数。 例如下面的函数
1 | /** |
padLeft存在一个问题,padding参数的类型指定成了any。 这就是说我们可以传入一个既不是number也不是string类型的参数,但是TypeScript却不报错。
1 | let indentedString = padLeft("Hello world", true); // 编译阶段通过,运行时报错 |
在传统的面向对象语言里,我们可能会将这两种类型抽象成有层级的类型。 这么做显然是非常清晰的,但同时也存在了过度设计。 padLeft原始版本的好处之一是允许我们传入原始类型。 这样做的话使用起来既简单又方便。 如果我们就是想使用已经存在的函数的话,这种新的方式就不适用了。
代替any, 我们可以使用联合类型做为padding的参数:
1 | /** |
联合类型表示一个值可以是几种类型之一。 我们用竖线(|)分隔每个类型,所以number | string | boolean表示一个值可以是number,string,或boolean。
如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。
1 | interface Bird { |
这里的联合类型可能有点复杂,但是你很容易就习惯了。 如果一个值的类型是A | B,我们能够确定的是它包含了A和B中共有的成员。 这个例子里,Bird具有一个fly成员。 我们不能确定一个Bird | Fish类型的变量是否有fly方法。 如果变量在运行时是Fish类型,那么调用pet.fly()就出错了。
在使用联合类型时,没有赋值只能访问联合类型中共有的方法和属性
如果是联合类型在使用方法的时候 只能采用公共的方法来使用
还是从安全性考虑, 在使用联合类型的时候 我门通过会先定义值,在使用保证安全
1 | let name:string | number // 联合类型 |
这里的!表示此值非空
1 | let ele: HTMLElement | null = document.getElementById('#app'); |
可辨识联合(Discriminated Unions)
你可以合并单例类型,联合类型,类型保护和类型别名来创建一个叫做可辨识联合的高级模式,它也称做标签联合或代数数据类型。 可辨识联合在函数式编程很有用处。 一些语言会自动地为你辨识联合;而TypeScript则基于已有的JavaScript模式。 它具有3个要素:
- 具有普通的单例类型属性—可辨识的特征。
- 一个类型别名包含了那些类型的联合—联合。
- 此属性上的类型保护。
1 | interface Square { |
首先我们声明了将要联合的接口。 每个接口都有kind属性但有不同的字符串字面量类型。 kind属性称做可辨识的特征或标签。 其它的属性则特定于各个接口。 注意,目前各个接口间是没有联系的。 下面我们把它们联合到一起:
1 | type Shape = Square | Rectangle | Circle; |
现在我们使用可辨识联合:
1 | function area(s: Shape) { |
四.类型断言
- 类型断言
类型断言有两种形式。 其一是“尖括号”语法
1 | let someValue: any = "this is a string"; |
另一个为as语法:
1 | let someValue: any = "this is a string"; |
我门断言类型后在使用 as断言成某种类型(一定是联合类型中的某一个) ! (非空断言, 表示这个值一定不是空的)不存在结果你自己承担, ts不管了,你认为的一定有值
1 | let name: string | number; |
!TS语法 ?js语法 链判断运算符(如果值有再去取值
1 | let ele = document.getElementById('app'); |
? 表示的是取值操作,不能赋值 , !表示某个变量一定存在
ele?.style.background
?? js语法 合并空值运算符, 三元表达式 但是 false || 取的结果
let val = 0 || 100 // 除了 null 和 undefiend 其他都是true
- 双重断言
尽量不要使用双重断言,会破坏原有类型关系,断言为any是因为any类型可以被赋值给其他类型
1 | type Direction = 'Up' | 'Down' | 'Left' | 'Right'; |
五.字面量类型
1 | type Direction = 'Up' | 'Down' | 'Left' | 'Right'; |
可以用字面量当做类型,同时也表明只能采用这几个值(限定值)。类似枚举。
字符串字面量类型
字符串字面量类型允许你指定字符串必须的固定值。 在实际应用中,字符串字面量类型可以与联合类型,类型保护和类型别名很好的配合。 通过结合使用这些特性,你可以实现类似枚举类型的字符串。
1 | type Easing = "ease-in" | "ease-out" | "ease-in-out"; |
你只能从三种允许的字符中选择其一来做为参数传递,传入其它值则会产生错误。
类型“uneasy”的参数不能赋值给类型“ease-in”|“ease-out”|“ease-in-out”的参数
1 | Argument of type '"uneasy"' is not assignable to parameter of type '"ease-in" | "ease-out" | "ease-in-out"' |
字符串字面量类型还可以用于区分函数重载:
1 | function createElement(tagName: "img"): HTMLImageElement; |
数字字面量类型
TypeScript还具有数字字面量类型。
1 | function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 { |
我们很少直接这样使用,但它们可以用在缩小范围调试bug的时候:
1 | function foo(x: number) { |
换句话说,当x与2进行比较的时候,它的值必须为1,这就意味着上面的比较检查是非法的。
类型别名
类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。
1 | type Name = string; |
起别名不会新建一个类型 - 它创建了一个新名字来引用那个类型。 给原始类型起别名通常没什么用,尽管可以做为文档的一种形式使用。
同接口一样,类型别名也可以是泛型 - 我们可以添加类型参数并且在别名声明的右侧传入:
1 | type Container<T> = { value: T }; // T 起别名 value |
我们也可以使用类型别名来在属性里引用自己:
1 | type Tree<T> = { |
与交叉类型一起使用,我们可以创建出一些十分稀奇古怪的类型。
1 | type LinkedList<T> = T & { next: LinkedList<T> }; |
然而,类型别名不能出现在声明右侧的任何地方。
1 | type Yikes = Array<Yikes>; // error 报错 |
接口 vs 类型别名
像我们提到的,类型别名可以像接口一样;然而,仍有一些细微差别。
其一,接口创建了一个新的名字,可以在其它任何地方使用。 类型别名并不创建新名字—比如,错误信息就不会使用别名。 在下面的示例代码里,在编译器中将鼠标悬停在interfaced上,显示它返回的是Interface,但悬停在aliased上时,显示的却是对象字面量类型。
1 | type Alias = { num: number } |
另一个重要区别是类型别名不能被extends和implements(自己也不能extends和implements其它类型)。 因为软件中的对象应该对于扩展是开放的,但是对于修改是封闭的,你应该尽量去使用接口代替类型别名。
另一方面,如果你无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名。
赋值推断
赋值时推断,类型从右像左流动,会根据赋值推断出变量类型
1 | let str = 'zhufeng'; |
返回值推断
自动推断函数返回值类型
1 | function sum(a: string, b: string) { |
函数推断
函数从左到右进行推断
1 | type Sum = (a: string, b: string) => string; |
属性推断
可以通过属性值,推断出属性的类型
1 | let person = { |
类型反推
可以使用typeof关键字反推变量类型
1 | let person = { |
索引访问操作符
1 | interface IPerson { |
类型映射
1 | interface IPerson { |
练习代码
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
// 声明类型的时候 如果没有标识类型 他是什么类型?
// 没有复制的变量默认值是undefined 但是类型是any
// const 是常量意味着定义的值不会修改所以他的类型是一个字面量类型 . const声明变量必须复制
// let 声明变量 可以修改所以类型范围推到的结果会变大
const a1 = 1
let a2 = 1
// 断言的问题
let strOrNum :string | number; // 如果是联合类型在使用方法的时候 只能采用公共的方法来使用
// 还是从安全性考虑, 在使用联合类型的时候 我门通过会先定义值,在使用. 保证安全
// 1) 指定类型在使用
// strOrNum = '1'
// strOrNum.endsWith();
// strOrNum = 1
// strOrNum.toFixed()
// 2) 我门断言类型后在使用 as断言成某种类型(一定是联合类型中的某一个) ! (非空断言, 表示这个值一定不是空的)
// 不存在结果你自己承担, ts不管了,你认为的一定有值
(strOrNum! as string).charCodeAt(0);
(<number>strOrNum!).toFixed(3);
// !TS语法 ?js语法 链判断运算符(如果值有再去取值)
let ele = document.getElementById('app');
// ele!.style.background = 'red';
(ele as HTMLElement).style.background = 'red';
// ? 表示的是取值操作,不能赋值 , !表示某个变量一定存在
// ele?.style.background
// ?? js语法 合并空值运算符, 三元表达式 但是 false || 取的结果
// let val = 0 || 100 // 除了 null 和 undefiend 其他都是true
// 值 as xxx 或者 <xxx>值 一般用于联合类型. 将大范围的类型 断言成子类型
(strOrNum! as unknown as boolean); // 双重断言,一般不建议使用,但是还会用到,破坏原有的关系
// 类型别名将类型提取出来
type Direction = 'up'|'down'|'left'|'right'; // 快速构建一个可以复用的类型
let direction:Direction
let up:'down' = direction! as 'down'
export {}
4.函数 (function)
函数中的类型 1)函数的声明方式 2) 函数的参数 3) 函数的返回值
function关键字来声明的函数可以提升到当前作用域顶部
对于ts来说有区别: 函数关键字声明的函数 不能标注函数类型
通过表达式来声明的函数: 必须赋予的值要满足定义的类型 (要求有一个兼容性在里面)
函数的两种声明方式
通过function关键字来进行声明
1
2
3
4function sum(a: string, b: string):string {
return a+b;
}
sum('a','b')可以用来限制函数的参数和返回值类型
通过表达式方式声明
1
2
3
4type Sum = (a1: string, b1: string) => string;
let sum: Sum = (a: string, b: string) => {
return a + b;
};
为函数定义类型
1 | function add(x: number, y: number): number { |
书写完整函数类型
1 | let myAdd: (x:number, y:number) => number = |
如果标明函数的类型,在使用函数的时候以标明的为准
只要参数类型是匹配的,那么就认为它是有效的函数类型,而不在乎参数名是否正确。
第二部分是返回值类型。 对于返回值,我们在函数和返回值类型之前使用(=>)符号,使之清晰明了。 如之前提到的,返回值类型是函数类型的必要部分,如果函数没有返回任何值,你也必须指定返回值类型为void而不能留空。
函数的类型只是由参数类型和返回值组成的。 函数中使用的捕获变量不会体现在类型里。 实际上,这些变量是函数的隐藏状态并不是组成API的一部分。
函数的推断类型
尝试这个例子的时候,你会发现如果你在赋值语句的一边指定了类型但是另一边没有类型的话,TypeScript编译器会自动识别出类型:
1 | // myAdd has the full function type myAdd具有完整的函数类型 |
这叫做“按上下文归类”,是类型推论的一种。 它帮助我们更好地为程序指定类型
可选参数和默认参数
TypeScript里的每个函数参数都是必须的。 这不是指不能传递null或undefined作为参数,而是说编译器检查用户是否为每个参数都传入了值。 编译器还会假设只有这些参数会被传递进函数。 简短地说,传递给一个函数的参数个数必须与函数期望的参数个数一致。
1 | function buildName(firstName: string, lastName: string) { |
可选参数
JavaScript里,每个参数都是可选的,可传可不传。 没传参的时候,它的值就是undefined。 在TypeScript里我们可以在参数名旁使用
?实现可选参数的功能。 比如,我们想让last name是可选的:1
2
3
4
5
6
7
8
9
10function buildName(firstName: string, lastName?: string) {
if (lastName)
return firstName + " " + lastName;
else
return firstName;
}
let result1 = buildName("Bob"); // 现在可以正常工作了
let result2 = buildName("Bob", "Adams", "Sr."); // error, 参数太多
let result3 = buildName("Bob", "Adams"); // ah, 刚刚好可选参数必须跟在必须参数后面。 如果上例我们想让first name是可选的,那么就必须调整它们的位置,把first name放在后面。
参数 可选参数
?可选参数 意味着可以不传 和 string | undefiend 必须得传。 可选参数只能在参数列表中的后面1
2
3
4
5
6
7
8type ISum = (a: string, b?: string) => string // 可选参数必须在其他参数的最后面
const sum = function(a:string,b:string = 'abc'){
return a+b
}
// 这里如果是兼容处理 采用的是自己标识的 不是你复制的类型
sum('a'); // 可选参数必须在其他参数的最后面默认参数
在TypeScript里,我们也可以为参数提供一个默认值当用户没有传递这个参数或传递的值是
undefined时。 它们叫做有默认初始化值的参数。 让我们修改上例,把last name的默认值设置为"Smith"。1
2
3
4
5
6
7
8function buildName(firstName: string, lastName = "Smith") {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // 现在工作正常,返回“Bob” Smith"
let result2 = buildName("Bob", undefined); // 仍然有效,也返回“ "Bob Smith"
let result3 = buildName("Bob", "Adams", "Sr."); // error, 参数太多
let result4 = buildName("Bob", "Adams"); // ah, 刚刚好在所有必须参数后面的带默认初始化的参数都是可选的,与可选参数一样,在调用函数的时候可以省略。 也就是说可选参数与末尾的默认参数共享参数类型
1
2
3function buildName(firstName: string, lastName?: string) {
// ...
}和
1
2
3function buildName(firstName: string, lastName = "Smith") {
// ...
}共享同样的类型
(firstName: string, lastName?: string) => string。 默认参数的默认值消失了,只保留了它是一个可选参数的信息。与普通可选参数不同的是,带默认值的参数不需要放在必须参数的后面。 如果带默认值的参数出现在必须参数前面,用户必须明确的传入
undefined值来获得默认值。 例如,我们重写最后一个例子,让firstName是带默认值的参数:1
2
3
4
5
6
7
8function buildName(firstName = "Will", lastName: string) {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // error, 参数太少
let result2 = buildName("Bob", "Adams", "Sr."); // error, 参数太多
let result3 = buildName("Bob", "Adams"); // okay 返回 "Bob Adams"
let result4 = buildName(undefined, "Adams"); // okay 返回 "Will Adams"1
2
3
4let sum = (a: string, b: string = 'b'): string => {
return a + b;
};
sum('a'); // 默认参数必须在其他参数的最后面1
2
3
4
5
6
7
8
9// 函数中有arguments 但是我们不建议使用
function sum(...args:number[]):number{ // (函数式编程 入参和返回值 组合式api) 函数 (不考虑使用this 和 arguments)
return args.reduce((memo,current)=>(memo += current,memo),0)
}
// 参数的类型直接参数后面:标识 函数的返回值在 {}前面来标识
const sum: (...args:any[])=> any = (...args:any[]) :any=> {
return
}
剩余参数
必要参数,默认参数和可选参数有个共同点:它们表示某一个参数。 有时,你想同时操作多个参数,或者你并不知道会有多少参数传递进来。 在JavaScript里,你可以使用arguments来访问所有传入的参数。
函数中有arguments 但是我们不建议使用
在TypeScript里,你可以把所有参数收集到一个变量里:
1 | function buildName(firstName: string, ...restOfName: string[]) { |
剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个。 编译器创建参数数组,名字是你在省略号(...)后面给定的名字,你可以在函数体内使用这个数组。
这个省略号也会在带有剩余参数的函数类型定义上使用到:
1 | function buildName(firstName: string, ...restOfName: string[]) { |
1 | const sum = (...args: string[]): string => { |
1 | function sum(...args:number[]):number{ // (函数式编程 入参和返回值 组合式api) 函数 (不考虑使用this 和 arguments) |
1 | // 参数的类型直接参数后面:标识 函数的返回值在 {}前面来标识 |
this
学习如何在JavaScript里正确使用this就好比一场成年礼。 由于TypeScript是JavaScript的超集,TypeScript程序员也需要弄清this工作机制并且当有bug的时候能够找出错误所在。 幸运的是,TypeScript能通知你错误地使用了this的地方。 如果你想了解JavaScript里的this是如何工作的,那么首先阅读Yehuda Katz写的Understanding JavaScript Function Invocation and “this”。 Yehuda的文章详细的阐述了this的内部工作原理,因此我们这里只做简单介绍。
this和箭头函数JavaScript里,
this的值在函数被调用的时候才会指定。 这是个既强大又灵活的特点,但是你需要花点时间弄清楚函数调用的上下文是什么。 但众所周知,这不是一件很简单的事,尤其是在返回一个函数或将函数当做参数传递的时候。下面看一个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17let deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
createCardPicker: function() {
return function() {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return {suit: this.suits[pickedSuit], card: pickedCard % 13};
}
}
}
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);可以看到
createCardPicker是个函数,并且它又返回了一个函数。 如果我们尝试运行这个程序,会发现它并没有弹出对话框而是报错了。 因为createCardPicker返回的函数里的this被设置成了window而不是deck对象。 因为我们只是独立的调用了cardPicker()。 顶级的非方法式调用会将this视为window。 (注意:在严格模式下,this为undefined而不是window)。为了解决这个问题,我们可以在函数被返回时就绑好正确的
this。 这样的话,无论之后怎么使用它,都会引用绑定的‘deck’对象。 我们需要改变函数表达式来使用ECMAScript 6箭头语法。 箭头函数能保存函数创建时的this值,而不是调用时的值:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18let deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
createCardPicker: function() {
// NOTE: the line below is now an arrow function, allowing us to capture 'this' right here
return () => {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return {suit: this.suits[pickedSuit], card: pickedCard % 13};
}
}
}
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);更好事情是,TypeScript会警告你犯了一个错误,如果你给编译器设置了
--noImplicitThis标记。 它会指出this.suits[pickedSuit]里的this的类型为any。
this参数
尽量不采用this 来作为函数的上下文, this的缺陷就是类型推导问题
如果想限制this类型 那么需要手动指定this类型
1 | function getValue(this: IPerson, key: IKeys) { // this不是行参 是标明this的类型 |
不幸的是,this.suits[pickedSuit]的类型依旧为any。 这是因为this来自对象字面量里的函数表达式。 修改的方法是,提供一个显式的this参数。 this参数是个假的参数,它出现在参数列表的最前面:
1 | function f(this: void) { |
让我们往例子里添加一些接口,Card 和 Deck,让类型重用能够变得清晰简单些:
1 | interface Card { |
现在TypeScript知道createCardPicker期望在某个Deck对象上调用。 也就是说this是Deck类型的,而非any,因此--noImplicitThis不会报错了。
回调函数里的this参数
当你将一个函数传递到某个库函数里在稍后被调用时,你可能也见到过回调函数里的this会报错。 因为当回调函数被调用时,它会被当成一个普通函数调用,this将为undefined。 稍做改动,你就可以通过this参数来避免错误。 首先,库函数的作者要指定this的类型:
1 | interface UIElement { |
this: void意味着addClickListener期望onclick是一个函数且它不需要一个this类型。 然后,为调用代码里的this添加类型注解:
1 | class Handler { |
指定了this类型后,你显式声明onClickBad必须在Handler的实例上调用。 然后TypeScript会检测到addClickListener要求函数带有this: void。 改变this类型来修复这个错误:
1 | class Handler { |
因为onClickGood指定了this类型为void,因此传递addClickListener是合法的。 当然了,这也意味着不能使用this.info. 如果你两者都想要,你不得不使用箭头函数了:
1 | class Handler { |
这是可行的因为箭头函数不会捕获this,所以你总是可以把它们传给期望this: void的函数。 缺点是每个Handler对象都会创建一个箭头函数。 另一方面,方法只会被创建一次,添加到Handler的原型链上。 它们在不同Handler对象间是共享的。
函数的重载
JavaScript本身是个动态语言。 JavaScript里函数根据传入不同的参数而返回不同类型的数据是很常见的。
ts 中函数有一个概念 叫重载(类型的重载), 对于强类型语言可以一个函数写多遍(参数不同) js实现重载考的是arguments
1 | let suits = ["hearts", "spades", "clubs", "diamonds"]; |
pickCard方法根据传入参数的不同会返回两种不同的类型。 如果传入的是代表纸牌的对象,函数作用是从中抓一张牌。 如果用户想抓牌,我们告诉他抓到了什么牌。 但是这怎么在类型系统里表示呢。
方法是为同一个函数提供多个函数类型定义来进行函数重载。 编译器会根据这个列表去处理函数的调用。 下面我们来重载pickCard函数。
1 | let suits = ["hearts", "spades", "clubs", "diamonds"]; |
这样改变后,重载的pickCard函数在调用的时候会进行正确的类型检查。
为了让编译器能够选择正确的检查类型,它与JavaScript里的处理流程相似。 它查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。
注意,function pickCard(x): any并不是重载列表的一部分,因此这里只有两个重载:一个是接收对象另一个接收数字。 以其它参数调用pickCard会产生错误。
1 | function toArray(value: string): string[]// 具体的某一种方案 |
练习代码
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// 函数中的类型 1)函数的声明方式 2) 函数的参数 3) 函数的返回值
// function关键字来声明的函数可以提升到当前作用域顶部
// 对于ts来说有区别: 函数关键字声明的函数 不能标注函数类型
// 通过表达式来声明的函数: 必须赋予的值要满足定义的类型 (要求有一个兼容性在里面)
// function sum(a,b){
// return a+b
// }
// 1)函数类型的定义 (a: any, b: any) => any | (a: any, b: any) : any
// type ISum = {(a: any, b: any) : any}
// type ISum = (a: any, b: any) => any
// const sum:ISum = function(a:string,b:string){
// return a+b
// }
// 如果标明函数的类型,在使用函数的时候以标明的为准
// 2)参数 可选参数 ? 可选参数 意味着可以不传 和 string | undefiend 必须得传。 可选参数只能在参数列表中的后面
// 默认值 =
// type ISum = (a: string, b?: string) => string
// const sum = function(a:string,b:string = 'abc'){
// return a+b
// }
// // 这里如果是兼容处理 采用的是自己标识的 不是你复制的类型
// sum('1')
// 3) 参数this问题
// 尽量不采用this 来作为函数的上下文, this的缺陷就是类型推导问题
// 如果想限制this类型 那么需要手动指定this类型
function getValue(this: IPerson, key: IKeys) { // this不是行参 是标明this的类型
return this[key];
}
// 我想根据值来获得类型 typeof, 配合type来声明新的类型
// keyof 可以获取对象中的类型 作为联合类型
const person = { name: 'jw', age: 30, address: '昌平霍营' }
type IPerson = typeof person; // 提取对象的类型为IPerson, type类型会提升到顶部
type IKeys = keyof IPerson;
// 可以将子类型赋予给父类型
getValue.call(person, 'age')
// 函数中有arguments 但是我们不建议使用
// function sum(...args:number[]):number{ // (函数式编程 入参和返回值 组合式api) 函数 (不考虑使用this 和 arguments)
// return args.reduce((memo,current)=>(memo += current,memo),0)
// }
// 参数的类型直接参数后面:标识 函数的返回值在 {}前面来标识
// const sum: (...args:any[])=> any = (...args:any[]) :any=> {
// return
// }
// ts 中函数有一个概念 叫重载(类型的重载), 对于强类型语言可以一个函数写多遍(参数不同) js实现重载考的是arguments
// 入参是一个字符串 或者是数字 -》 【‘字符串’】 【‘数字’】
// string[] | number[] (number|string)[]
function toArray(value: string): string[]// 具体的某一种方案
function toArray(value: number): number[];
// 上面的声明仅仅是类型上的重载
function toArray(value: string | number): string[] | number[] { // 所有的实现
return []
}
let arr1 = toArray('abc')
let arr2 = toArray(123)
export { }
4.类(class)
类: 类的组成: 构造函数、属性(实例属性,原型属性、静态属性)、方法 (实例的方法,原型方法,静态方法) 访问器, 静态相关的配置
TS中定义类
1 | class Pointer{ |
实例上的属性需要先声明在使用,构造函数中的参数可以使用可选参数和剩余参数
1 | class Greeter { |
如果你使用过C#或Java,你会对这种语法非常熟悉。 我们声明一个Greeter类。这个类有3个成员:一个叫做greeting的属性,一个构造函数和一个greet方法。
你会注意到,我们在引用任何一个类成员的时候都用了this。 它表示我们访问的是类的成员。
最后一行,我们使用new构造了Greeter类的一个实例。 它会调用之前定义的构造函数,创建一个Greeter类型的新对象,并执行构造函数初始化它。
继承
在TypeScript里,我们可以使用常用的面向对象模式。 基于类的程序设计中一种最基本的模式是允许使用继承来扩展现有的类。
看下面的例子:
1 | class Animal { |
这个例子展示了最基本的继承:类从基类中继承了属性和方法。 这里,Dog是一个派生类,它派生自Animal基类,通过extends关键字。 派生类通常被称作子类,基类通常被称作超类。
因为Dog继承了Animal的功能,因此我们可以创建一个Dog的实例,它能够bark()和move()。
下面我们来看个更加复杂的例子。
1 | class Animal { |
这个例子展示了一些上面没有提到的特性。 这一次,我们使用extends关键字创建了Animal的两个子类:Horse和Snake。
与前一个例子的不同点是,派生类包含了一个构造函数,它必须调用super(),它会执行基类的构造函数。 而且,在构造函数里访问this的属性之前,我们一定要调用super()。 这个是TypeScript强制执行的一条重要规则。
这个例子演示了如何在子类里可以重写父类的方法。 Snake类和Horse类都创建了move方法,它们重写了从Animal继承来的move方法,使得move方法根据不同的类而具有不同的功能。 注意,即使tom被声明为Animal类型,但因为它的值是Horse,调用tom.move(34)时,它会调用Horse里重写的方法:
1 | Slithering... |
类中的修饰符
public 修饰符(公开属性)
public 公开属性,类的实例在外部可以访问这个属性,类的内部也可以访问,继承的子类也可以访问
1 | class Animal { |
1 | class Animal { |
我们可以通过参数属性来简化父类中的代码
protected修饰符 (自己和子类可以访问到)
我自己能访问,儿子能访问,外部无法访问
1 | class Animal { |
private修饰符 (私有的)
除了自己都访问不到
1 | class Animal { |
readonly修饰符 (仅读)
readonly 标识仅读属性,意味着如果初始化后,不能被修改
1 | class Animal { |
参数属性
在上面的例子中,我们不得不定义一个受保护的成员name和一个构造函数参数theName在Person类里,并且立刻给name和theName赋值。 这种情况经常会遇到。参数属性可以方便地让我们在一个地方定义并初始化一个成员。 下面的例子是对之前Animal类的修改版,使用了参数属性:
1 | class Animal { |
注意看我们是如何舍弃了theName,仅在构造函数里使用private name: string参数来创建和初始化name成员。 我们把声明和赋值合并至一处。
参数属性通过给构造函数参数添加一个访问限定符来声明。 使用private限定一个参数属性会声明并初始化一个私有成员;对于public和protected来说也是一样。
存取器
TypeScript支持通过getters/setters来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。
下面来看如何把一个简单的类改写成使用get和set。 首先,我们从一个没有使用存取器的例子开始。
1 | class Employee { |
我们可以随意的设置fullName,这是非常方便的,但是这也可能会带来麻烦。
下面这个版本里,我们先检查用户密码是否正确,然后再允许其修改员工信息。 我们把对fullName的直接访问改成了可以检查密码的set方法。 我们也加了一个get方法,让上面的例子仍然可以工作。
1 | let passcode = "secret passcode"; |
我们可以修改一下密码,来验证一下存取器是否是工作的。当密码不对时,会提示我们没有权限去修改员工。
对于存取器有下面几点需要注意的:
首先,存取器要求你将编译器设置为输出ECMAScript 5或更高。 不支持降级到ECMAScript 3。 其次,只带有get不带有set的存取器自动被推断为readonly。 这在从代码生成.d.ts文件时是有帮助的,因为利用这个属性的用户会看到不允许够改变它的值。
静态属性和方法
创建类的静态成员,这些属性存在于类本身上面而不是类的实例上。 在这个例子里,我们使用static定义origin,因为它是所有网格都会用到的属性。 每个实例想要访问这个属性的时候,都要在origin前面加上类名。 如同在实例属性上使用this.前缀来访问属性一样,这里我们使用Grid.来访问静态属性。
1 | class Grid { |
1 | class Animal { |
静态属性和静态方法是可以被子类所继承的
1 | class Animal { |
Super属性 (继承 extends)
1 | class Animal { |
super在构造函数 指向的是父类,在原型的方法中调用的时候指向的是父类的原型
super在 类中访问 constructor / static函数中执指向的都是父类 ,在原型方法中, 属性访问器都是 父类的原型
1 | // super在构造函数 指向的是父类,在原型的方法中调用的时候指向的是父类的原型 |
1 | class Singleton{ |
在TypeScript里,我们可以使用常用的面向对象模式。 基于类的程序设计中一种最基本的模式是允许使用继承来扩展现有的类。
看下面的例子:
1 | class Animal { |
这个例子展示了最基本的继承:类从基类中继承了属性和方法。 这里,Dog是一个派生类,它派生自Animal基类,通过extends关键字。 派生类通常被称作子类,基类通常被称作超类。
因为Dog继承了Animal的功能,因此我们可以创建一个Dog的实例,它能够bark()和move()。
下面我们来看个更加复杂的例子。
1 | class Animal { |
这个例子展示了一些上面没有提到的特性。 这一次,我们使用extends关键字创建了Animal的两个子类:Horse和Snake。
与前一个例子的不同点是,派生类包含了一个构造函数,它必须调用super(),它会执行基类的构造函数。 而且,在构造函数里访问this的属性之前,我们一定要调用super()。 这个是TypeScript强制执行的一条重要规则。
这个例子演示了如何在子类里可以重写父类的方法。 Snake类和Horse类都创建了move方法,它们重写了从Animal继承来的move方法,使得move方法根据不同的类而具有不同的功能。 注意,即使tom被声明为Animal类型,但因为它的值是Horse,调用tom.move(34)时,它会调用Horse里重写的方法:
1 | Slithering... |
类的装饰器
装饰类
1 | function addSay(target:any){ |
装饰类可以给类扩展功能,需要开启
experimentalDecorators:true
装饰类中属性
1 | function toUpperCase(target:any,key:string){ |
装饰属性可以对属性的内容进行改写,装饰的是实例属性则target指向类的原型、装饰的是静态属性则target执行类本身~
装饰类中方法
1 | function noEnum(target:any,key:string,descriptor:PropertyDescriptor){ |
装饰参数
1 | function addPrefix(target:any,key:string,paramIndex:number){ |
抽象类
抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。 不同于接口,抽象类可以包含成员的实现细节。 abstract关键字是用于定义抽象类和在抽象类内部定义抽象方法。
1 | abstract class Animal { |
抽象类中的抽象方法不包含具体实现并且必须在派生类中实现。 抽象方法的语法与接口方法相似。 两者都是定义方法签名但不包含方法体。 然而,抽象方法必须包含abstract关键字并且可以包含访问修饰符。
1 | abstract class Department { |
抽象类无法被实例化,只能被继承,抽象方法不能在抽象类中实现,只能在抽象类的具体子类中实现,而且必须实现。
1 | abstract class Animal{ |
定义类型时
void表示函数的返回值为空(不关心返回值类型,所有在定义函数时也不关心函数返回值类型)
ts 中有抽象类概念, abstract 不存在的
抽象类 可以含义非抽象的方法和属性 , 不会new它 , 抽象类可以被继承,抽象类中抽象方法子类必须要实现
1 | { |
高级技巧
构造函数
当你在TypeScript里声明了一个类的时候,实际上同时声明了很多东西。 首先就是类的实例的类型。
1 | class Greeter { |
这里,我们写了let greeter: Greeter,意思是Greeter类的实例的类型是Greeter。 这对于用过其它面向对象语言的程序员来讲已经是老习惯了。
我们也创建了一个叫做构造函数的值。 这个函数会在我们使用new创建类实例的时候被调用。 下面我们来看看,上面的代码被编译成JavaScript后是什么样子的:
1 | let Greeter = (function () { |
上面的代码里,let Greeter将被赋值为构造函数。 当我们调用new并执行了这个函数后,便会得到一个类的实例。 这个构造函数也包含了类的所有静态属性。 换个角度说,我们可以认为类具有实例部分与静态部分这两个部分。
让我们稍微改写一下这个例子,看看它们之前的区别:
1 | class Greeter { |
这个例子里,greeter1与之前看到的一样。 我们实例化Greeter类,并使用这个对象。 与我们之前看到的一样。
再之后,我们直接使用类。 我们创建了一个叫做greeterMaker的变量。 这个变量保存了这个类或者说保存了类构造函数。 然后我们使用typeof Greeter,意思是取Greeter类的类型,而不是实例的类型。 或者更确切的说,”告诉我Greeter标识符的类型”,也就是构造函数的类型。 这个类型包含了类的所有静态成员和构造函数。 之后,就和前面一样,我们在greeterMaker上使用new,创建Greeter的实例。
把类当做接口使用
如上一节里所讲的,类定义会创建两个东西:类的实例类型和一个构造函数。 因为类可以创建出类型,所以你能够在允许使用接口的地方使用类。
1 | class Point { |
练习代码
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// 类: 类的组成: 构造函数、属性(实例属性,原型属性、静态属性)、方法 (实例的方法,原型方法,静态方法) 访问器, 静态相关的配置
class Circle {
// 给这个类来声明属性
private x:number
public y:number
public fn: ()=> void
constructor(x:number,y:number = 200){ // 函数
this.x = x;
this.y = y;
this.fn = ()=>{}
}
}
// 类的修饰符
// public 公开属性,类的实例在外部可以访问这个属性,类的内部也可以访问,继承的子类也可以访问
// protected (我自己能访问,儿子能访问,外部无法访问)
// private (私有的 自己能访问)
// 平时我们一般采用public 或者private的场景比较多.
// readonly 标识仅读属性,意味着如果初始化后,不能被修改
// let circle = new Circle(100,100)
class Animal {
constructor(protected name:string){ // 等价于 每个属性增添了public
}
// 原型方法 就是每一个实例共享的方法 , 父类提供的方法 子类是可以进行方法重写的
// 原型的函数:void 意味着是不关心函数的返回值,并不是空的意思
protected changeName(value:string,age:number):void{
this.name = value
}
// 原型属性 需要通过访问器来实现
get aliasName(){
return '$' + this.name;
}
set alisName(name:string){
this.name = name;
}
static a = 1;
static getA(){
return this.a
}
}
// super在构造函数 指向的是父类,在原型的方法中调用的时候指向的是父类的原型
class Cat extends Animal {
constructor(name:string,public readonly age:number){
super(name); // Animal.call(this)
// this.age = age;
}
// 子类在重写父类方法要兼容, 赋予的函数可以兼容父类
protected changeName(value:string){
super.changeName(value,100)
return 'abc'
}
}
const tom1 = new Cat('tom',30)
const tom2 = new Cat('tom',30)
console.log(Cat.getA());
// console.log(tom1.aliasName === tom2.aliasName)
// tom.changeName('jerry')
// 以上用法同es6
// super在 类中访问 constructor / static函数中执指向的都是父类 ,在原型方法中, 属性访问器都是 父类的原型
class Singleton{
static instance = new Singleton(); // 自己本身创造一个实例,后续一致用这一个,不产生多个
protected constructor(){ } // 增加protected之后 构造函数不能被new了
static getInstance(){
return this.instance
}
}
// 类在什么时候 不用外面new
let instance = Singleton.getInstance();
// ts 中有抽象类概念, abstract 不存在的
// 抽象类 可以含义非抽象的方法和属性 , 不会new它 , 抽象类可以被继承,抽象类中抽象方法子类必须要实现
{
abstract class Animal{
public abstract a:string
drink(){ // 非抽象,已经有实现了
console.log('喝水')
}
abstract eat():void // 抽象的方法,父类没有实现,那么子类必须实现
}
class Cat extends Animal{
public a!: string;
eat(): void {
throw new Error("Method not implemented.");
}
}
// 因为我们编写的代码的时候 , 慢慢的脱离继承了。 组合优于继承。 类的装饰器(redux,nest, mobx)
}
export {}
5.接口(interface)
文档的解释
1 | function printLabel(labelledObj: { label: string }) { |
类型检查器会查看printLabel的调用。 printLabel有一个参数,并要求这个对象参数有一个名为label类型为string的属性。 需要注意的是,我们传入的对象参数实际上会包含很多属性,但是编译器只会检查那些必需的属性是否存在,并且其类型是否匹配。 然而,有些时候TypeScript却并不会这么宽松,我们下面会稍做讲解。
下面我们重写上面的例子,这次使用接口来描述:必须包含一个label属性且类型为string:
1 | interface LabelledValue { |
LabelledValue接口就好比一个名字,用来描述上面例子里的要求。 它代表了有一个label属性且类型为string的对象。 需要注意的是,我们在这里并不能像在其它语言里一样,说传给printLabel的对象实现了这个接口。我们只会去关注值的外形。 只要传入的对象满足上面提到的必要条件,那么它就是被允许的。
还有一点值得提的是,类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以。
可选属性
接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。 可选属性在应用“option bags”模式时很常用,即给函数传入的参数对象中只有部分属性赋值了。
下面是应用了“option bags”的例子:
1 | interface SquareConfig { |
带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个?符号。
可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获引用了不存在的属性时的错误。 比如,我们故意将createSquare里的color属性名拼错,就会得到一个错误提示:
1 | interface SquareConfig { |
只读属性
一些对象属性只能在对象刚刚创建的时候修改其值。 你可以在属性名前用readonly来指定只读属性:
1 | interface Point { |
你可以通过赋值一个对象字面量来构造一个Point。 赋值后,x和y再也不能被改变了。
1 | let p1: Point = { x: 10, y: 20 }; |
TypeScript具有ReadonlyArray<T>类型,它与Array<T>相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改:
1 | let a: number[] = [1, 2, 3, 4]; |
上面代码的最后一行,可以看到就算把整个ReadonlyArray赋值到一个普通数组也是不可以的。 但是你可以用类型断言重写:
1 | a = ro as number[]; |
readonly vs const
最简单判断该用readonly还是const的方法是看要把它做为变量使用还是做为一个属性。 做为变量使用的话用const,若做为属性则使用readonly。
额外的属性检查
我们在第一个例子里使用了接口,TypeScript让我们传入{ size: number; label: string; }到仅期望得到{ label: string; }的函数里。 我们已经学过了可选属性,并且知道他们在“option bags”模式里很有用。
然而,天真地将这两者结合的话就会像在JavaScript里那样搬起石头砸自己的脚。 比如,拿createSquare例子来说:
1 | interface SquareConfig { |
注意传入createSquare的参数拼写为colour而不是color。 在JavaScript里,这会默默地失败。
你可能会争辩这个程序已经正确地类型化了,因为width属性是兼容的,不存在color属性,而且额外的colour属性是无意义的。
然而,TypeScript会认为这段代码可能存在bug。 对象字面量会被特殊对待而且会经过额外属性检查,当将它们赋值给变量或作为参数传递的时候。 如果一个对象字面量存在任何“目标类型”不包含的属性时,你会得到一个错误。
1 | // error: 'colour' not expected in type 'SquareConfig' |
绕开这些检查非常简单。 最简便的方法是使用类型断言:
1 | let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig); |
然而,最佳的方式是能够添加一个字符串索引签名,前提是你能够确定这个对象可能具有某些做为特殊用途使用的额外属性。 如果SquareConfig带有上面定义的类型的color和width属性,并且还会带有任意数量的其它属性,那么我们可以这样定义它:
1 | interface SquareConfig { |
我们稍后会讲到索引签名,但在这我们要表示的是SquareConfig可以有任意数量的属性,并且只要它们不是color和width,那么就无所谓它们的类型是什么。
还有最后一种跳过这些检查的方式,这可能会让你感到惊讶,它就是将这个对象赋值给一个另一个变量: 因为squareOptions不会经过额外属性检查,所以编译器不会报错。
1 | let squareOptions = { colour: "red", width: 100 }; |
要留意,在像上面一样的简单代码里,你可能不应该去绕开这些检查。 对于包含方法和内部状态的复杂对象字面量来讲,你可能需要使用这些技巧,但是大部额外属性检查错误是真正的bug。 就是说你遇到了额外类型检查出的错误,比如“option bags”,你应该去审查一下你的类型声明。 在这里,如果支持传入color或colour属性到createSquare,你应该修改SquareConfig定义来体现出这一点。
函数类型
接口能够描述JavaScript中对象拥有的各种各样的外形。 除了描述带有属性的普通对象外,接口也可以描述函数类型。
为了使用接口表示函数类型,我们需要给接口定义一个调用签名。 它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。
1 | interface SearchFunc { |
这样定义后,我们可以像使用其它接口一样使用这个函数类型的接口。 下例展示了如何创建一个函数类型的变量,并将一个同类型的函数赋值给这个变量。
1 | let mySearch: SearchFunc; |
对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配。 比如,我们使用下面的代码重写上面的例子:
1 | let mySearch: SearchFunc; |
函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的。 如果你不想指定类型,TypeScript的类型系统会推断出参数类型,因为函数直接赋值给了SearchFunc类型变量。 函数的返回值类型是通过其返回值推断出来的(此例是false和true)。 如果让这个函数返回数字或字符串,类型检查器会警告我们函数的返回值类型与SearchFunc接口中的定义不匹配。
1 | let mySearch: SearchFunc; |
可索引的类型
与使用接口描述函数类型差不多,我们也可以描述那些能够“通过索引得到”的类型,比如a[10]或ageMap["daniel"]。 可索引类型具有一个索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。 让我们看一个例子:
1 | interface StringArray { |
上面例子里,我们定义了StringArray接口,它具有索引签名。 这个索引签名表示了当用number去索引StringArray时会得到string类型的返回值。
共有支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用number来索引时,JavaScript会将它转换成string然后再去索引对象。 也就是说用100(一个number)去索引等同于使用"100"(一个string)去索引,因此两者需要保持一致。
1 | class Animal { |
字符串索引签名能够很好的描述dictionary模式,并且它们也会确保所有属性与其返回值类型相匹配。 因为字符串索引声明了obj.property和obj["property"]两种形式都可以。 下面的例子里,name的类型与字符串索引类型不匹配,所以类型检查器给出一个错误提示:
1 | interface NumberDictionary { |
最后,你可以将索引签名设置为只读,这样就防止了给索引赋值:
1 | interface ReadonlyStringArray { |
你不能设置myArray[2],因为索引签名是只读的。
类 类型
实现接口
与C#或Java里接口的基本作用一样,TypeScript也能够用它来明确的强制一个类去符合某种契约。
1 | interface ClockInterface { |
你也可以在接口中描述一个方法,在类里实现它,如同下面的setTime方法一样:
1 | interface ClockInterface { |
接口描述了类的公共部分,而不是公共和私有两部分。 它不会帮你检查类是否具有某些私有成员。
类静态部分与实例部分的区别
当你操作类和接口的时候,你要知道类是具有两个类型的:静态部分的类型和实例的类型。 你会注意到,当你用构造器签名去定义一个接口并试图定义一个类去实现这个接口时会得到一个错误:
1 | interface ClockConstructor { |
这里因为当一个类实现了一个接口时,只对其实例部分进行类型检查。 constructor存在于类的静态部分,所以不在检查的范围内。
因此,我们应该直接操作类的静态部分。 看下面的例子,我们定义了两个接口,ClockConstructor为构造函数所用和ClockInterface为实例方法所用。 为了方便我们定义一个构造函数createClock,它用传入的类型创建实例。
1 | interface ClockConstructor { |
因为createClock的第一个参数是ClockConstructor类型,在createClock(AnalogClock, 7, 32)里,会检查AnalogClock是否符合构造函数签名。
继承接口
和类一样,接口也可以相互继承。 这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。
1 | interface Shape { |
一个接口可以继承多个接口,创建出多个接口的合成接口。
1 | interface Shape { |
混合类型
先前我们提过,接口能够描述JavaScript里丰富的类型。 因为JavaScript其动态灵活的特点,有时你会希望一个对象可以同时具有上面提到的多种类型。
一个例子就是,一个对象可以同时做为函数和对象使用,并带有额外的属性。
1 | interface Counter { |
在使用JavaScript第三方库的时候,你可能需要像上面那样去完整地定义类型。
接口继承类
当接口继承了一个类类型时,它会继承类的成员但不包括其实现。 就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。 接口同样会继承到类的private和protected成员。 这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现(implement)。
当你有一个庞大的继承结构时这很有用,但要指出的是你的代码只在子类拥有特定属性时起作用。 这个子类除了继承至基类外与基类没有任何关系。 例:
1 | class Control { |
在上面的例子里,SelectableControl包含了Control的所有成员,包括私有成员state。 因为state是私有成员,所以只能够是Control的子类们才能实现SelectableControl接口。 因为只有Control的子类才能够拥有一个声明于Control的私有成员state,这对私有成员的兼容性是必需的。
在Control类内部,是允许通过SelectableControl的实例来访问私有成员state的。 实际上,SelectableControl就像Control一样,并拥有一个select方法。 Button和TextBox类是SelectableControl的子类(因为它们都继承自Control并有select方法),但Image和Location类并不是这样的。
学习的解释
接口可以在面向对象编程中表示行为的抽象,也可以描述对象的形状。 接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。 (接口中不能含有具体的实现逻辑)
接口: 抽象类(有抽象的也有非抽象) 接口必须都是抽象的(没有具体的实现)
接口的概念 就是描述数据的结构、或者形状的, 定义好结构,在去针对结构来进行实现
type 和 interface 区别
一般情况下 描述对象,类 我门采用interface更多一些 ,interface无法声明联合类型
type 可以快速声明类型 联合类型,工具类型只能采用type , type不能重名
type用的更多,能用type 就type 不能用interface. 复杂类型采用type
函数接口参数
1 | const fullName = ({firstName,lastName}:{firstName:string,lastName:string}):string =>{ |
我们可以约束函数中的参数,但是类型无法复用
1 |
|
我们可以通过接口进行描述
函数类型接口
1 | interface IFullName { |
通过接口限制函数的参数类型和返回值类型
函数混合类型
1 | interface ICounter { |
对象接口
对象接口可以用来描述对象的形状结构
1 | interface IVegetables { |
?标识的属性为可选属性,
readOnly标识的属性则不能修改。多个同名的接口会自动合并断言的方式来进行赋值, 用的最多 as IVeg
接口的合并 同名的会进行合并,自定义类型的时候 会使用。 自己的业务逻辑用的比较少
可以扩展一个新类型 在来使用。 可以扩展属性
任意类型 随机的属性 描述数字索引的 (除了必有的属性之外 ,其他任意)
兼容性
1 | const tomato:IVegetables = { |
任意属性、可索引接口
1 | interface Person { |
任意属性可以对某一部分必填属性做限制,其余的可以随意增减
1 | interface IArr { |
可索引接口可以用于标识数组
类接口
这里先来强调一下抽象类和接口的区别,抽象类中可以包含具体方法实现。接口中不能包含实现
接口可以实现, 接口的实现都是通过类来实现 , 接口中一个类 可以实现多个接口
一个接口可以继承多个接口, 接口可以用于继承类
1 | interface Speakable { |
一个类可以实现多个接口,在类中必须实现接口中的方法和属性
1 | interface SpeakChinese { |
类类型, 不能藐视类本身,描述的是实例
类的类型 需要通过typeof 来取类型
ts的校验规则 鸭子类型检测
1 | // 描述构造函数 |
接口继承
1 | interface Speakable { |
构造函数类型
1 | interface Clazz { |
这里无法标识返回值类型
1 | interface Clazz<T> { |
new() 表示当前是一个构造函数类型,这里捎带使用了下泛型。 在使用
createClass时动态传入类型
练习代码
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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145// 接口: 抽象类(有抽象的也有非抽象) 接口必须都是抽象的(没有具体的实现)
// 接口的概念 就是描述数据的结构、或者形状的, 定义好结构,在去针对结构来进行实现
// type 和 interface 区别
// 一般情况下 描述对象,类 我门采用interface更多一些 ,interface无法声明联合类型
// type 可以快速声明类型 联合类型,工具类型只能采用type , type不能重名
// type用的更多,能用type 就type 不能用interface. 复杂类型采用type
// 1) 接口可以描述对象结构 (子可以赋予给父)
// interface IPerson {
// username: string // 类型,不是具体实现
// age:number
// }
// // 子可以赋予给父亲。 我们需要把一个值赋予给另一个值。 如果是声明的必须必须一致
// let obj = {
// username:'abc',
// age:30,
// address:'地址'
// }
// let person:IPerson = obj; // 赋值的时候 会产生兼容性 (儿子可以赋予给父亲)
// 2) 接口可以描述函数
// interface ICounter {
// ():number
// count:number
// }
// // const 标识此值不能修改 let 可以修改的 (如果给函数增加类型定义 函数不能被修改时只能用const)
// const counter:ICounter = () =>{
// return counter.count++
// }
// counter.count = 0;
// 1)可以通过?表示接口的属性 可有可无
// interface IVeg {
// name: string
// taste: string;
// size: number,
// color?: string
// }
// 2)断言的方式来进行赋值, 用的最多 as IVeg
// 3)接口的合并 同名的会进行合并,自定义类型的时候 会使用。 自己的业务逻辑用的比较少
// 4) 可以扩展一个新类型 在来使用。 可以扩展属性
// 5)任意类型 随机的属性 描述数字索引的 (除了必有的属性之外 ,其他任意)
// 6)兼容性
interface IVeg {
readonly name: string // 只读属性
taste: string;
size: number,
}
interface IVegetable extends IVeg {
color?: '红色',
[prop: string]: any // 任意接口 key 随意,值随意
}
const veg: IVegetable = {
name: '西红柿',
taste: '甜',
size: 50,
color: '红色',
'a': 1,
'b': '2',
[Symbol()]: 'ABC',
0: 1
}
interface IArray { // 索引接口
[key: number]: any
}
// let arr:IArray = [1,2,3]
let arr: IArray = { 0: 1, 1: 2, 2: 'abc', 3: true }
interface ResponseData {
username: string,
token: string
}
interface ReturnVal {
code: number,
data: ResponseData
}
// 通过索引访问符 来获取内部类型
type ICode = ReturnVal['code'];
type IUsername = ReturnVal['data']['username']; // 可以用于取值的类型
type IKeys = ReturnVal[keyof ReturnVal]; // 取值的类型, 可以采用这种方式
// 接口可以实现, 接口的实现都是通过类来实现 , 接口中一个类 可以实现多个接口
// 一个接口可以继承多个接口, 接口可以用于继承类
interface SpeakChinese {
speakChinese(): void
}
interface SpeakEnglish {
speakEnglish(): void
}
class Speak {
public a!: string;
}
interface Speakable extends SpeakEnglish, SpeakChinese, Speak {
speak(): void // 实现的是原型方法
// speak:()=>void // 实现的是实例方法
}
class Speaker implements Speakable {
public a!: string;
speakEnglish(): void {
throw new Error("Method not implemented.");
}
speakChinese(): void {
throw new Error("Method not implemented.");
}
speak() {
return 100
}
}
// 我门如何表示我要传入的是一个类
class Dog {
constructor(public name: string) { } // new
}
class Cat {
constructor(public name: string) { }
}
// 类类型, 不能藐视类本身,描述的是实例
// 类的类型 需要通过typeof 来取类型
// ts的校验规则 鸭子类型检测
// 描述构造函数
// interface IClazz<T>{
// new (name:string): T
// }
type IClazz<T> = new (name: string) => T
function createInstance<T>(clazz: IClazz<T>, name: string) {
return new clazz(name)
}
const instance = createInstance(Cat, 'tom')
// 泛型: 泛型坑位 (函数的形式参数) 刚开始类型不确定,通过使用的时候 来确定类型
export { }








