TypeScript 是什么?
- TypeScript 是 JavaScript 的超集,具有可选的类型并且可编译成纯 JavaScript.
- TypeScript 提供的静态类型系统,在编译阶段通过类型检测,可以避免很多线上(运行时)的 bug.
- so…. TypeScript 可以写出更易读、更健壮、更可维护的大型项目

安装和编译
ts 代码是不能直接在浏览器环境或者 node 环境下运行。我们需要将它编译成普通 js 代码。
全局安装 & 编译
npm install typescript -g ## 全局安装;mac可能需要sudotsc hello.ts ## 对.ts文件进行编译tsc hello1.ts hello2.ts ## 多个ts同时编译node hello.js ## 执行编译后的js代码tsc --watch ## 文件保存自动编译tsc --init ## 生成配置文件。当然,你也可以手动创建 tsconfig.json
学习过程中,编译成 ts 还需要 node 命令去执行 js 文件。是不是很麻烦?
npm install ts-node -g ## ts编译 & node执行 一步到位ts-node hello.ts
当我们尝试去 ts-node hello.ts 时,可能会报错 'Error: Cannot find module '@types/node/package.json' 。 此时我们再装一个即可 npm install -g @types/node 。
重启 iterm2,此时就可以继续 ts-node hello.ts !
在学习开发的过程中,经常会遇到编辑器提示变量被重复申明。
解决方式:
① 换个变量名
② 可以在 ts 文件中添加 export {}
基础数据类型
TypeScript 中一个叫类型,一个叫值。一般冒号后面的为类型,等号后面的为值。
Boolean 类型
const isDone: boolean = true
Number 类型
const age: number = 18
String 类型
const name: string = 'cc'
Array 类型
// 实现方式一:在类型后面加上 []const list: number[] = [1, 2, 3]// 实现方式二:数组泛型const list1: Array<number> = [1, 2, 3]
Tuple 元组类型
元组类型:用来表示已知元素数量和类型的数组,各元素的类型不必相同,对应位置的类型需要相同。
const p: [string, number] = ['cc', 18];// 关于越界的元素。也就是在已知的 tuple 去添加新的元素时,只能添加已有类型的集合// 例如 上面的例子 只能是 string | numberp.push('beijing'); // okp.push(true); // no ok
那么问题来了。 数组 vs 元组
| 元组 | 数组 |
|---|---|
| 每一项可以是不同的类型 | 每一项都是同一类型(联合类型除外) |
| 有预定义的长度 | 没有长度限制 |
| 用于表示一个固定的结构 | 用于表示一个列表 |
Tuple 实际应用场景:Excel ?
Enum 枚举类型
考虑某个变量所有可能存在的值,用自然语言的单词去表示它每一个值。清晰地表达意图。
比如:颜色、方位、星期等等
普通枚举
enum Direction {LEFT,RIGHT,TOP,BOTTOM}
来瞅瞅以上枚举代码编译成 js 长啥样。
var Direction;(function (Direction) {Direction[Direction["LEFT"] = 0] = "LEFT";Direction[Direction["RIGHT"] = 1] = "RIGHT";Direction[Direction["TOP"] = 2] = "TOP";Direction[Direction["BOTTOM"] = 3] = "BOTTOM";})(Direction || (Direction = {}));// 枚举值 和 枚举名会进行一个反向映射. 例如console.log(Direction['LEFT'] == Direction[0]); // trueconsole.log(Direction[0] == 'LEFT'); // true;
默认情况下。
第一个值为 LEFT 为 0,其他枚举成员会开始递增 +1;
Direction[0] 为 LEFT;
与此同时枚举值和枚举名会反向映射。 Direction[‘LEFT’] 为 0。
可以给任意枚举成员设置初始值。
enum Direction {LEFT,RIGHT = 6,TOP,BOTTOM}
编译一下结果一目了然。第一个值还是默认为0。第二值被初始化为6。剩下未手动赋值的枚举成员会接着上一个枚举项递增 +1
var Direction;(function (Direction) {Direction[Direction["LEFT"] = 0] = "LEFT"; // 将 0 赋予 LEFT,然后又将 LEFT 赋予 0Direction[Direction["RIGHT"] = 6] = "RIGHT";Direction[Direction["TOP"] = 7] = "TOP";Direction[Direction["BOTTOM"] = 8] = "BOTTOM";})(Direction || (Direction = {}));
注意避免覆盖的情况, 例如
enum Direction {LEFT = 2,RIGHT = 1,TOP,BOTTOM}// 按照逻辑,未手动赋值的枚举成员会接着上一个枚举项递增 +1// 也就是 此时的 TOP 应该是 2。就与 LEFT 的枚举值重复了。// ts 并不会报错。 所以我们在使用枚举的时候尽量避免这种情况。
初始值除了整数。小数/负数、字符串可不可以?可以!try it!
enum Direction {LEFT = 'LEFT',RIGHT = 6,TOP = 1.2, // 未手动赋值的前一项 必须为数值类型BOTTOM // 自然 +1 => 2.2}
枚举项有两种类型:常数项(constant member)和计算所得项(computed member)。简单的说,我们前面的例子都是属于常数项,要么就是直接赋值,要么就是默认值。那么什么是计算所得项呢?
const xx = () => 2enum Colors {RED,YELLOW,BLUE = 'blue'.length,GREEN = xx()}
字符串的长度是不是需要计算一下子,函数执行是不是也得算算。特别要注意一点儿的是,计算所得项后面不能跟没有赋值的枚举项。如果紧接在计算所得项后面的是未手动赋值的项,那么它就会因为无法获得初始值而报错。就是计算所得项后面的枚举项必须指明,可继续添加计算所得项或者添加枚举值(数值类型)。
了解常数枚举
常数枚举与普通枚举的区别就是,它会在编译阶段被删除,并且不能包含计算成员。
// const 声明的枚举类型const enum Colors {Red,Yellow,Blue}const getColors = [Colors.Red, Colors.Yellow, Colors.Blue];// var getColors = [0 /* Red */, 1 /* Yellow */, 2 /* Blue */];
Any 任意类型
定义为 any 类型是可以赋值给任意类型,当类型转换遇到困难或者数据结构复杂难以定义可以使用 any。any 给了我们很大的自由,如果所有类型定义为any,TypeScript 其实就失去的它存在的意义了。
any 类型太宽松了。很多场景会编写出类型正确编译通过,but 运行时报错的代码。例如
let cc: any;cc.trim();new cc();cc.cc.eat(); // 完全可以通过编译,但是运行明显是会报错
Unknow 类型
在 typescript 中,当我们不确定一个类型是什么类型,可以选择给其声明为 any 或者 unknow。但实际上,ts 更推荐使用 unknown, 因为unknown可以保证类型安全。如果使用 any ,其实是放弃了类型检查。
let cc: unknown;cc = true; // okcc = 'yes'; // okcc = 100; // ok
我们发现类型 unknown 和 any 。对它进行任何类型的赋值都是正确的。 但是我们尝试将 unknown 赋值给其他类型值会怎么样呢?
let cc: unknown;let a: unknown = cc; // oklet b: any = cc; // oklet c: string = cc; // no oklet d: number = cc; // no ok// ...
unknown 类型只能被赋值给 any 类型和 unknown 类型本身。让我们看看当我们尝试对类型为 unknown 的值执行操作时会发生什么。
let cc: unknown;cc.trim(); // no oknew cc(); // no okcc.cc.eat(); // no ok
对比后我们知道,any 可以随意取值赋值操作,并不会进行任何类型的检查。但是 unknown 就不一样了,它需要先进行断言或者typeof等来进行判断类型
let str: unknown;// str.length // 直接取 .length 肯定不ok 。但是 any 可以// 如果要取 length, 我们可以将 str 断言成 string, 或者 intanceof 判断为 String 类型if (str instanceof String) {console.log(str.length) // ok}// console.log((str as string).length) // ok
这下我们知道 unknown 为什么是安全的了。它只是指明了类型还未确认,后续还需要去断言,也就是它并未放弃类型检查。
Void 类型
表示没有任何类型。当函数没有返回值的时候,TypeScript 会认为返回值类型为 void
function getName(): void {console.log('Hello, my name is CC !')}
还有一个点值得注意的是,变量声明为 void 类型本身就没啥作用,它的值只能是 undefined 或者 null。但是 null 需要将配置文件 strictNullChecks 设为 false.
Null 和 Undefined
null 和 undefined 是其他类型的子类型。说白就是你可以将 null undefined 赋值给 number string boolean 等类型。
// 需要将配置文件 strictNullChecks 设为 false.let count: number = 1count = undefinedcount = null
Never 类型
never 表示永远不会出现的值的类型。返回 never 的函数必须存在无法到达的终点
- 抛出异常
- 无限循环 ```typescript function error(msg: string): never { throw new Error(msg) }
function infiniteLoop(): never { while(true) {} }
function check(x: string | number) { if (typeof x === ‘number’) { console.log(x) // number } else if (typeof x === ‘string’) { console.log(x) // string } else { console.log(x) // never } }
strictNullChecks 开还关呢? 建议打开。严格的校验能让代码更加的健壮。<a name="aosp0"></a>###<a name="lrlPd"></a>## 类型推论有时候没有明确指明类型,但是 ts 还是能帮我们推断出一个类型```typescriptlet count = 100 // ts 推断出 count 是一个 number 类型count = '100' // 编译报错
有一个要特别注意的是,当在声明变量的时候没有给变量赋值时,该变量会被推断成 any 类型。从而跳过类型检查。
let xx = 1x = '1'// 这段代码完全 ok 的。 编译时不会报错
联合类型
联合类型表示取值可以为多种类型中的某一种。
let money: number | stringmoney = 10000money = '1w'
当 ts 还不知道联合类型变量到底是哪一种变量时候,那么我们只能访问该联合类型共有的属性和方法。
function getMoney(money: string | number) {console.log(money.length) // 编译报错,因为 number 没有length 属性console.log(money.toString()) // 编译成功,因为 toString 是 number 和 string 的共有方法}
交叉类型
类型断言
- 类型断言可以将一个联合类型的变量,指定为一个更加具体的类型
- 不能将联合类型断言成不存在的类型,当然除了 any
有时候你会比 TypeScript 更了解某个值的详细信息。通常发生在你会更新清楚地知道一个比它现在更确切的类型。需要注意的是,类型断言只能够「欺骗」TypeScript 编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误。
断言的两种写法
- 值 as 类型
- <类型>值 (在React开发中不采用这种方式,因为在React中<>会被认为是一个ReactElement)
function getMoney(money: string | number) {console.log((money as number).toFixed(2))console.log((money as string).length)console.log((money as boolean)) // 断言成不存在的值, no okconsole.log((money as any as boolean)) // 通过 双重断言 欺骗编译器console.log((money as any))}
类型别名
类型别名用来给一个类型起个新名字。 关键词 type ,常用于联合类型。
我们类型别名和接口很像,那有什么差异呢?官网说:type Msg = string | string []function toast(msg: Msg) {// ...}
Because an ideal property of software is being open to extension, you should always use an interface over a type alias if possible.(因为完美的软件是对拓展开放,所以只要可能,你通常更需要使用接口而不是别名类型。)
If you can’t express some shape with an interface and you need to use a union or tuple type, type aliases are usually the way to go.(如果你无法用接口表示类型,同时你需要使用联合类型或元组,那么别名类型会是经常使用的方式。)
小结一下:
- 优先使用接口
- 如果需要联合类型或者元组等类型处理,则可以使用别名类型声明
- 当不涉及接口继承(extends)、类实现(implement)时,接口和别名只作为类型定义时,两者皆可。
字面量类型
字面量类型来约束只能取某几个字符串中的一个
let direction: 'LEFT';direction = 'LEFT'; // 只能是 LEFTtype Direction = 'LEFT' | 'RIGHT' | 'TOP' | 'BOTTOM' // 类型别名function getDirection(d: Direction): void {console.log(d)}getDirection('RIGHT') // 传入的参数值只能 Direction 中的一个
类型别名 和 字面量类型都是用 type 进行定义。
类型守卫
函数类型
函数类型可以指定参数个数、参数类型和返回值类型。
function greeting(name: string): string {return 'Hello, ' + name}
函数表达式
type Greeting = (name: string) => string // 此处的箭头表示的是函数返回类型const greeting: Greeting = (name: string): string => {return 'Hello, ' + name}
可选参数
function greeting(name: string, age?: number): void {}
默认参数
function ajax(url: string, method:string = 'GET'):void {}
剩余参数
function pushArray(arr: number[], ...items: number[]): number[] {items.forEach(item => arr.push(item))return arr}
函数的重载
java 函数重载:函数名相同,参数不同,返回类型可以相同也可以不同。
ts 函数重载:以相同函数名,参数不同(参数类型不同、参数个数不同或参数个数相同时参数的先后顺序不同) 来创建多个函数的方式,当调用函数时,根据实参的形式,选择与它匹配的方法进行执行。
type addType = string | number;function add(a: number, b: number): number; // 函数声明,属于重载列表function add(a: string, b: string): string; // 函数声明,属于重载列表function add(a: addType, b: addType): addType { // 函数实现,紧跟在函数声明后面,不属于重载列表if (typeof a === 'string' || typeof b === 'string') {return a.toString() + b.toString()}return a + b;}add(1, 2);add('1', '2');add(1, '2'); // 编译报错add('1', 2); // 编译报错
注意:
- 重载可以理解为一个函数提供多个函数定义,最后一个函数必须为函数的实现,并且必须紧跟在函数定义后面。
- 当 TypeScript 编译器处理函数重载时,它会查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,多个函数定义如果有包含关系,一定要把最精确的定义放在最前面。
当然,除了普通函数有重载,类的方法也有重载。
重写 vs 重载 ?
- 重写是指子类重写继承自父类中的方法
- 重载是指为同一个函数提供多个类型定义
类
在 ES6 以前我们是通过构造函数来实现「类」的概念。ES6 之后,引入了 class。TypeScript 除了 ES6 中类的功能外,还加入了一些新的用法。
其实和 ES6 中类的基本定义和用法都一样的, 比如类的定义、构造函数的定义、类的继承、存储器的用法、静态方法、实例的生成都是一毛一样的用法。TypeScript 可能加入之前说 方法的重载,引入了 pulic、protected等修饰符。
类的基本定义
- class 定义类
- constructor 定义构造函数
- new 生成实例,并且会自动调用 构造函数
```typescript
class User {
myName: string; // 当需要 this.myName 时,就需要先在这里定义好该属性。
myAge: number;
myCity: string;
constructor(myName: string, myAge: number, myCity: string) {
this.myAge = myAge; this.myCity = myCity; } getName(): void {this.myName = myName;
} }console.log(this.myName)
const u1 = new User(‘cc’, 10, ‘beijing’) u1.getName(); console.log(u1.myAge);
以上类定义可以通过 public 这个关键词来简化。看一下是不是省掉很多行代码了。```typescriptclass User {// 等同于:类中定义该属性,同时给该属性赋值,使代码更简洁了// 此处如果不写类型会报: 参数“myCity”隐式具有“any”类型// 可以将 tscofig.js noImplicitAny 改为 false。但不太建议,还是乖乖写上类型吧constructor(public myName: string, public myAge: number, public myCity: string) {}getName(): void {console.log(this.myName)}}const u1 = new User('cc', 10, 'beijing')u1.getName(); // ccconsole.log(u1.myAge); // 10
定义存取器
class User {constructor(public myName: string) {}get name() {return this.myName;}set name(newName) {this.myName = newName;}}const u1 = new User('cc')console.log(u1.name); // ccu1.name = 'cpc';console.log(u1.name); // cpc
静态属性、静态方法
在类里面通过 static 来实现静态属性和静态方法。静态属性和静态方法的调用或者访问方式,是直接 类.静态属性 和 类.静态方法
class User {static mood: string = 'happy';static getMood(): string {return this.mood;}}console.log(User.mood); // happyconsole.log(User.getMood()); // happy
继承
通过 extends 关键字来实现继承,子类中使用 super 关键字来调用父类的构造函数和方法。
类的继承包含继承父类的静态(static)、非静态的属性和方法。
class User {name: string;age: number;constructor(name: string, age: number) {this.name = name;this.age = age;}getName(): string {return this.name;}}// 通过 extends 来实现继承class Student extends User {city: string;constructor(name: string, age: number, city: string) {super(name, age); // 调用父类中的 constructor(name, age)this.city = city;}greeting(): string {return 'Hello, ' + super.getName(); // 调用父类中的 getName}}const s1 = new Student('cc', 10, 'beijing');console.log(s1.getName());console.log(s1.greeting());
思考: 当子类中存在和父类一样的方法名时,会报错吗?如果不会,调用该方法会执行子类方法还是父类方法呢?(理解为重写父类方法)
继承 vs 多态 ?
- 继承:子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性
- 多态:由继承而产生了相关的不同的类,对同一个方法可以有不同的行为
修饰符
修饰符是用来来修饰属性和方法的。
- public 公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public。(自己、子类和任何地方)
- private 私有的,不能在声明它的类的外部访问。(只能自己访问)
- protected 受保护的,它和 private 类似,区别是它在子类中也是允许被访问的。(只能自己和子类访问)
// 此处无代码,请自行脑补。。。
思考:
- 在类中,属性、方法、参数等没有明确指明修饰符,统统被认为是 public
- 如果被定义成 private 或者 protected 的属性,在实例想访问该怎么办呢? (gexXXX)
- 如果 构造函数 添加 protected 或者 private 会是怎样的呢? 静态属性和方法也添加上又会是怎样的呢?
readonly
- readonly 顾名思义就是只读
- 可以结合以上的修饰符一起使用,并将其放在修饰符之后, eg.
private readonly money: number - ts 同样允许将 interface、type、 class 上的属性标识为 readonly
- readonly vs const : readonly 实际上只是在编译阶段进行代码检查。而 const 则会在运行时检查。
class User {constructor(public readonly name: string) {}}const u1 = new User('cc');console.log(u1.name); // 读取很oku1.name = 'cpc'; // 设置就费劲儿
抽象类
abstract 用于定义抽象类和其中的抽象方法。
- 抽象类不允许实例化,就是不能 new
- 抽象类和方法不包含具体实现,必须在子类中实现。能不能不实现,不行,抽象类有几个方法子类就得实现几个方法
- 抽象方法也只能出现在抽象类中
- 子类可以对抽象类进行不同的实现
abstract class User {constructor(public name: string, public age: number) {}abstract greeting(): string;abstract eating(): void;working() { // 抽象类中是可以出现非抽象方法的console.log('make money!')}}class Teacher extends User {constructor(name: string, age: number, private gender: string) {super(name, age);}greeting() { // 继承抽象类,抽象方法必须一一实现,缺一不可。console.log('hello, ' + this.name);return 'hey man!' // 需和抽象方法返回类型保持一致}eating() {console.log(this.name + '正在吃午饭...');}}const t1 = new Teacher('cc', 10, 'male');t1.greeting();t1.eating();t1.working();
观察一下上面的代码的编译结果:
var __extends = (this && this.__extends) || (function () {var extendStatics = function (d, b) {extendStatics = Object.setPrototypeOf ||({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };return extendStatics(d, b);};return function (d, b) {if (typeof b !== "function" && b !== null)throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");extendStatics(d, b);function __() { this.constructor = d; }d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());};})();var User = /** @class */ (function () {function User(name, age) {this.name = name;this.age = age;}User.prototype.working = function () {console.log('make money!');};return User;}());var Teacher = /** @class */ (function (_super) {__extends(Teacher, _super);function Teacher(name, age, gender) {var _this = _super.call(this, name, age) || this;_this.gender = gender;return _this;}Teacher.prototype.greeting = function () {console.log('hello, ' + this.name);return '';};Teacher.prototype.eating = function () {console.log(this.name + '正在吃午饭...');};return Teacher;}(User));var t1 = new Teacher('cc', 10, 'male');t1.greeting();t1.eating();t1.working();
我们只看抽象类 User,编译结果中我们发现:
- 即使是抽象类,仍然会存在这个类;
- 抽象方法编译结果并不存在;
装饰器
装饰器是啥呢?
- 装饰器是一种特殊类型的声明,它能够被附加到类声明、方法、属性或参数上,可以修改其行为;
- 装饰器是一个表达式@expression这种形式,这个表达式被执行后,会返回一个函数;
- 函数的入参分别为 target、name 和 descriptor;
- 执行该函数后,可能返回 descriptor 对象,用于配置 target 对象;
说白了,可以将装饰器理解为在原有的代码逻辑外层再加点儿额外的东西(逻辑)。就是装饰一下嘛 !
装饰器可分为:
- 类装饰器
- 属性装饰器
- 方法装饰器
- 参数装饰器
装饰器有哪些写法?
- 普通装饰器(不可传参)
- 装饰器工厂(可以传参)
类装饰器
// target: 被装饰的类declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
// 定义一个普通装饰器function log(target: Function): void {console.log('logger...')target.prototype.hello = function(): void {console.log('hello, cc!')}}// 报错: 对修饰器的实验支持功能在将来的版本中可能更改。在 "tsconfig" 或 "jsconfig" 中设置 "experimentalDecorators" 选项以删除此警告。// 使用装饰器之前 需要去 tsconfig.json 开一下配置 experimentalDecorators: true@log // 装饰器的使用方式:紧挨着需要被装饰的类。class User {hello!: () => void;}const u1 = new User(); // 输出 logger...u1.hello(); // 输出 hello, cc!
每次实例化的时候都会输出一样的 log。 我们可以自定义传入参数吗? 是可以的。就是前面说的装饰器工厂。
// 装饰器工厂定义一个装饰器function logFactory(msg: string) {return function(target: Function): void {console.log(msg)}}@logFactory('This is a logger...') // 实现传参的方式来执行一个装饰器class User {}const u1 = new User();
属性装饰器
// target: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象// propertyKey: 被装饰类的属性名declare type PropertyDecorator = (target:Object,propertyKey: string | symbol) => void;
function toUpperCase(target: any, propertyKey: string) {let value = target[propertyKey] // 获取到修饰参数的值const getter = function() {return value;}const setter = function(newVal: string) {value = newVal.toUpperCase()}// 删除已有属性,添加新的属性值,并把setter也替换了if (delete target[propertyKey]) {Object.defineProperty(target, propertyKey, {get: getter,set: setter,enumerable: true,configurable: true})}}class User {@toUpperCase // 注意装饰器后面不能写分号name = 'cc'}const u1 = new User();console.log(u1.name); // CC
方法装饰器
// target: 被装饰的类// propertyKey: 方法名// descriptor: 属性描述符declare type MethodDecorator = <T>(target:Object,propertyKey: string | symbol,descriptor: TypePropertyDescript<T>) => TypedPropertyDescriptor<T> | void;
function MethodDecorator(target: any, property: string, descriptor: PropertyDescriptor) {console.log(target);console.log(property);console.log(descriptor);}class User {name: string = 'cc'@MethodDecoratorgetName() {console.log(this.name)}}
参数装饰器
declare type ParameterDecorator = (target: Object,propertyKey: string | symbol,parameterIndex: number) => void
function addAge(target: any, methodName: string, paramsIndex: number) {console.log(target);console.log(methodName);console.log(paramsIndex);target.age = 10;}class User {age!: number;login(username: string, @addAge password: string) {console.log(this.age, username, password);}}const u1 = new User();u1.login('xxx', '123456')
装饰器执行顺序
/*有多个参数装饰器时:从最后一个参数依次向前执行;方法和方法参数中参数装饰器先执行;类装饰器总是最后执行;方法和属性装饰器,谁在前面谁先执行。因为参数属于方法一部分,所以参数会一直紧紧挨着方法执行.*/function Class1Decorator() {return function (target: any) {console.log("类1装饰器");}}function Class2Decorator() {return function (target: any) {console.log("类2装饰器");}}function MethodDecorator() {return function (target: any, methodName: string, descriptor: PropertyDescriptor) {console.log("方法装饰器");}}function Param1Decorator() {return function (target: any, methodName: string, paramIndex: number) {console.log("参数1装饰器");}}function Param2Decorator() {return function (target: any, methodName: string, paramIndex: number) {console.log("参数2装饰器");}}function PropertyDecorator(name: string) {return function (target: any, propertyName: string) {console.log(name + "属性装饰器");}}@Class1Decorator()@Class2Decorator()class Person {@PropertyDecorator('name')name: string = 'cc';@PropertyDecorator('age')age: number = 10;@MethodDecorator()greet(@Param1Decorator() p1: string, @Param2Decorator() p2: string) { }}/**name属性装饰器age属性装饰器参数2装饰器参数1装饰器方法装饰器类2装饰器类1装饰器*/
接口
- 接口一方面可以在面向对象编程中表示为
行为的抽象,另外还可以用来描述对象的形状 - 接口就是把一些类中共有的属性和方法抽象出来,可以用来约束实现(implements)此接口的类
- 一个类可以继承(extends)另一个类并实现(implements)多个接口
- 一个类可以实现多个接口,一个接口也可以被多个类实现,但一个类的可以有多个子类,但只能有一个父类
对象的形状
// 接口可以用来描述`对象的形状`,少属性或者多属性都会报错interface Person {name: string;age: number;gender?: string; // ? 表示可选属性speak(msg: string): void;}const p1: Person = {// name: 'cc', // 少属性也会报错age: 10,speak(msg: string) {console.log(msg)},address: '', // 多属性报错}
行为的抽象
// 接口可以在面向对象编程中表示为行为的抽象interface Eating {eat(): void;}interface Playing {play(): void;}// 一个类可以实现多个接口class Children implements Eating, Playing {eat() {}play() {}}
任意属性
当我们无法预先知道哪些新的属性,可以这么干
interface Person {readonly id: number; // 只读name: string;gender?: string;[propName: string]: any}// 这里要特别注意的是 [propName: string] 的类型必须包含已有的类型,比如这个例子有 number | string// 有可选参数时,需要将 undefined 也加上// 所以这个例子可以这么写 [propName: string]: number | string | undefined// propName 是随意取的,叫 props 也可,其他也行
接口的继承
interface Eatable {eat(): void;}interface EatKFC extends Eatable {eatKFC(): void;}class WorkHardMan implements EatKFC {eat() {console.log('吃点儿东西')}eatKFC() {console.log('努力的人吃KFC')}}
思考: 如果定义的两个名字一样的接口会是怎么样? try it. 盲猜不同属性会合并,相同属性类型需要一致
函数类型接口
对函数传入的参数个数、参数类型和函数返回值进行约束。
interface reverseFunc {(msg: string): string;}const reverse: reverseFunc = function(msg: string): string {return msg.split('').reverse().join('');}
思考:以下两个接口定义含义分别是什么?
interface T1 {(name: string): string;}interface T2 {s: (name: string) => string;}// T1 描述的是一个函数// T2 描述的是一个对象,有一个 s 属性,这个属性对应一个函数const t1: T1 = (name: string): string => name;const t2: T2 = {s: (name: string): string => name}
描述类接口
interface Eating {name: string;eat(sth: string): void;}class User implements Eating {constructor(public name: string) {}eat(sth: string): void {console.log('正在吃...' + sth)}}const u = new User('cc')u.eat('ice')
构造函数类型接口
可以使用 interface 里特殊的 new 关键字来描述类的构造函数类型
class Car {constructor(public size: string) {}}interface CarClass {new(size: string): Car; // 不加 new 是修饰函数的, 加 new 是修饰类的}function createCars(clazz: CarClass, size: string) {return new clazz(size)}const car = createCars(Car, 'large')console.log(car.size)// 当我们定义一个类时会得到两种类型// 类的类型 和 实例类型// 以下代码都是没毛病的const c: Car = new Car('mini');const cc: typeof Car = Car;
泛型
软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。
在像 C# 和 Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。
设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:
- 类的实例成员
- 类的方法
- 函数参数
- 函数返回值
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
泛型函数
需求:创建一个长度为 length 的数组,数组里面的值用 value 来填充
function createArray(length: number, value: any): any[] {const res = []for(let i = 0; i < length; i++) {res.push(value) // i}return res}const res = createArray(5, 'x')console.log(res)
思考:以上代码可以编译通过,但是我们发现返回的是一个 any 类型的数组。我们完全可以在函数内部改变修改返回数组的类型。 是不是就缺乏一些约束,或者对应关系。例如当我们传入一个 number 类型,我们只知道任何类型的数组都有可能被返回。。。我们更希望返回数组的类型应该和传入 value 的类型保持一致。
这个时候「泛型」就派上用场了。。。
function createArray<T>(length: number, value: T): T[] {const res: T[] = []for(let i = 0; i < length; i++) {res.push(value)}return res}const res = createArray(5, 'x');console.log(res)
观察以上泛型代码:
1、我们可以看到如何定义一个泛型。在函数名后面添加一个
2、T 可以理解为类型的形参,我们会在使用的是传递一个具体的类型
3、当然,T 并不是固定了,当然你可以 ABC… 任由你喜欢。其实这些大写字母并没有什么本质的区别,只不过是一个约定好的规范而已。 下面我们介绍一下一些常见泛型变量代表的意思:
- T(Type):表示一个 TypeScript 类型
- K(Key):表示对象中的键类型
- V(Value):表示对象中的值类型
- E(Element):表示元素类型
4、说好的使用的时候在传递类型,但以上代码并没有看到主动传递类型。因为类型推导会自动推算出来
多个类型参数
在使用泛型的时候,我们可以一次性定义多个类型参数。
需求:我们想定义一个 swap 函数,用来交换输入的元组。
function swap<A, B>(tuple: [A, B]): [B, A] {return [tuple[1], tuple[0]]}console.log(swap([1, '2']))
泛型类
需求:创建一个类,实例方法 push 可以添加值,实例方法会 getRandom 会随机一个已添加过的值
class MyArray<T> {private arr: T[] = []// ...push(value: T) {this.arr.push(value)}getRandom(): T {return this.arr[Math.floor(Math.random() * this.arr.length)]}}const my = new MyArray();my.push(1);my.push(2);my.push(99);console.log(my.getRandom())
观察以上代码:
当创建实例的时候并没有主动传入 类型。 此时的 T 是啥类型???
泛型接口
来看一个例子:
interface Calc {<T>(a: T, b: T): T}const sum: Calc = function<T>(a: T, b: T): T {return a}sum<number>(1, 2);
思考:返回的是单个 a, 如果返回的是 a 和 b 的运算结果呢? 运算符“+”不能应用于类型“T”和“T” . 泛型 T 可以是任意类型,因为不知道会传什么东西,所以不能相加。 那咋整?
在来看一个几乎类似的例子:我只是将类型形参 T 提升到了接口上面定义。
interface Calc<T> {(a: T, b: T): T}const sum: Calc<number> = function(a: number, b: number): number {return a + b;}sum(1, 2)
思考:泛型定义在 接口上 和 定义在接口里的函数上 有什么区别呢?
定义在接口上时,我们在使用接口的时候就必须先明确传递一个类型。
定义在接口里的函数时,我们在定义「形状」为该接口的函数时,继续保留 泛型,直到调用该函数时在明确传递类型。
默认泛型
我们之前把泛型理解为类型的形参,当然默认泛型,自然也可以理解为默认参数了
function createArray<T = string>(length: number, value: T): T[] {const res: T[] = []for(let i = 0; i < length; i++) {res.push(value)}return res}const res = createArray(5, 1);
观察以上代码:
是不是有疑惑,默认泛型为 string,创建时候确实没传递具体类型,是不是应该走 string,但传入一个 1 为 number 类型编译却可以通过。
其实最开始的时候我们说过不传递也 ts 也会根据类型推导,知道传递的是什么类型。当可以推导出来类型时,推导 > 默认。
看看另外一个例子,也是之前的例子,泛型类
class MyArray<T = string> {private arr: T[] = []// ...push(value: T) {this.arr.push(value)}getRandom(): T {return this.arr[Math.floor(Math.random() * this.arr.length)]}}const my = new MyArray(); // 此处没有传递类型,也推导不出具体类型,走了默认类型 stringmy.push(1); // 编译报错my.push(2);my.push(99);console.log(my.getRandom())
或者
interface T1<T> {}type T2 = T1; // 报错 泛型类型“T1<T>”需要 1 个类型参数。// 方案一:type T2 = T1<string>// 方案二:interface T1<T = string> {}
泛型约束
在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法:
function logger<T>(arg: T): T {console.log(arg.length); // 并不是任何类型都有 length 属性,所以编译报错return arg;}
我们可以对泛型进行约束,只允许这个函数传入那些包含 length 属性的变量。这就是泛型约束:
interface Lengthwise {length: number;}function logger<T extends Lengthwise>(arg: T): T {console.log(arg.length);return arg;}logger(1) // 虽然以上的定义编译是没问题的,但是在调用logger时,也要保证传入值有 length 属性
保证有length 属性??? 那我。。。
const obj = {length: 6}type hasLengthObj = typeof obj // 得到一份包含length形状的类型logger<hasLengthObj>('1')
so。。。 可以理解为类型有一个 length 属性就行 ok 。 不关心你是以什么方式去得到的。
再看 T extends Lengthwise。 T 继承 Lengthwise,T 是 Lengthwise 的子类型,T 需要满足 Lengthwise 的约束条件。可以理解为 T 所拥有的形状只准多,不准少于 Lengthwise 的形状。
interface Lengthwise {length: number;}const obj = {length: 6,age: 6}type hasLengthObj = typeof objlet l1: Lengthwise = { length: 2 };let l2: hasLengthObj = { length: 6, age: 6 };l1 = l2;// l2 = l1;
联合类型 和 接口/对象 的区别
泛型工具类型
typeof
在 TypeScript 中,typeof 操作符可以用来获取一个变量声明或对象的类型。
interface Person {name: string;age: number;}const man: Person = {name: 'cc',age: 10}type Human = typeof man // -> Personconst women: Human = {name: 'xx',age: 9}
function toArray(x: number): Array<number> {return [x];}type Func = typeof toArray; // -> (x: number) => number[]
typeof 我的理解是 可以 copy 一份完整的类型格式(或者说形状)。
keyof
keyof 操作符可以用来一个对象中的所有 key 值:
interface Person {name: string;age: number;}type K1 = keyof Person;type K2 = keyof Person[];type K3 = keyof { [x: string]: Person }; // 索引签名参数类型必须为 "string" 或 "number"。
in
in 用来遍历枚举类型:
type Keys = "a" | "b" | "c"type Obj = {[p in Keys]: any}
Partial
Partial
type Partial<T> = { [P in keyof T]?: T[P] };interface A {a1: string;a2: number;a3: boolean;}type aPartial = Partial<A>;const a: aPartial = {}; // 不会报错
首先通过 keyof T 拿到 T 的所有属性名,然后使用 in 进行遍历,将值赋给 P,最后通过 T[P] 取得相应的属性值。中间的 ? 号,用于将所有属性变为可选。
Readonly
type Readonly<T> = { readonly [P in keyof T]: T[P] };
….Partial、Required、Readonly、Record 和 ReturnType
