我正在参加「掘金·启航计划」
一、基础用法
初始化 TypeScript 工作环境的简易步骤
- 安装 TypeScript
- 用 tsc --init 初始化配置
- 编辑 tsconfig.json 配置 TypeScript 选项
TypeScript 的优势
- 规避大量低级错误,避免时间浪费,省时
- 减少多人协作项目的成本,大型项目友好,省力
- 良好代码提示,不用反复文件跳转或者翻文档,省心
TypeScript的原始类型
- 布尔类型:
boolean - 数字类型:
number - 字符串类型:
string - 空值:
void - Null 和 Undefined:
null和undefined - Symbol 类型:
symbol - BigInt 大数整数类型:
bigint
number: 二进制、十进制、十六进制等数都可以用 number 类型表示
const decLiteral: number = 6
const hexLiteral: number = 0xf00d
const binaryLiteral: number = 0b1010
const octalLiteral: number = 0o744
void: 表示没有任何类型,当一个函数没有返回值时,你通常会见到其返回值类型是 void: undefined是个例外能赋值给void****
const a:void = undefined
function warnUser(): void {
alert("this is my message");
}
symbol: 我们在使用 Symbol 的时候,必须添加 es6 的编译辅助库
TypeScript的高级类型
计算机类型系统理论中的顶级类型:
- any
- unknown
类型系统中的底部类型:
- never
非原始类型(non-primitive type)
- object
还有比较常见的数组、元组等等。
注解:
any: any类型是多人协作项目的大忌,很可能把Typescript变成AnyScript,通常在不得已的情况下,不应该首先考虑使用此类型。
unknown:
- 他跟
any的共同点,它跟any一样,可以是任何类型 - 在对
unknown类型的值执行大多数操作之前,我们必须进行某种形式的检查,而在对any类型的值执行操作之前,我们不必进行任何检查 - 当
unknown类型被确定是某个类型之前,它不能被进行任何操作比如实例化、getter、函数执行等等 - 「类型保护」可以执行
unknown
// 对 value 变量的所有赋值都被认为是类型正确的
let value: unknown;
value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK
value = Math.random; // OK
value = null; // OK
value = undefined; // OK
value = new TypeError(); // OK
value = Symbol("type"); // OK
// unknown 类型只能被赋值给 any 类型和 unknown 类型本身
let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // Error
let value4: number = value; // Error
let value5: string = value; // Error
let value6: object = value; // Error
let value7: any[] = value; // Error
let value8: Function = value; // Error
// 将 value 变量类型设置为 unknown 后,这些操作都不再被认为是类型正确的
value.foo.bar; // Error
value.trim(); // Error
value(); // Error
new value(); // Error
value[0][1]; // Error
never: 表示的是那些永不存在的值的类型,never 类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是 never 的子类型或可以赋值给 never 类型(除了never本身之外)。既是any也不能赋值给never。
数组: 例如number[]、string[]这种表示方式,或者Array这种表示方式。
元组(tuple):
元组非常严格,即使类型的顺序不一样也会报错。可以把元组看成严格版的数组,比如[string, number]我们可以看成是:
元组越界问题,当使用push之类的操作时是可以运行,并且可以打印出来,但是我们直接去访问新加入的元素比如:Tuple[2]时则会报错。
object: 普通对象、枚举、数组、元组通通都是 object 类型。
枚举
我们声明一个枚举类型是,虽然没有给它们赋值,但是它们的值其实是默认的数字类型,而且默认从0开始依次累加,因此当我们把第一个值赋值后,后面也会根据第一个值进行累加。
枚举可以反向映射:console.log(Direction[10]); // Up
用js表示枚举的原理:
因为 Direction[Direction["Up"] = 10] = "Up" 也就是 Direction[10] = "Up" ,所以我们可以把枚举类型看成一个JavaScript对象,而由于其特殊的构造,导致其拥有正反向同时映射的特性。
### 接口interface
interface user {
name: string
age?: number // 可选属性
readonly isMale: boolean // 只读属性
say: (word: string) => string // 函数
[propName: string]: any // 字符串索引签名,user可以有任意数量的属性,并且只要不是width,那么就无所谓他们的类型是什么了
}
如果两个对象中同一个字段(对象)内部的字段不同,则可以使用类似这种方法:
接口可以继承多个或一个接口:
interface VipUser extends user {
vip: string
}
// 用来表示一个对象的接口:
interface OBJ {
[prop: string]: any
}
类class
抽象类
抽象类做为其它派生类的基类使用,它们一般不会直接被实例化,不同于接口,抽象类可以包含成员的实现细节。abstract 关键字是用于定义抽象类和在抽象类内部定义抽象方法。
我们不能直接实例化抽象类,通常需要我们创建子类继承基类,然后可以实例化子类。
访问限定符
传统面向对象语言通常都有访问限定符,TypeScript 中有三类访问限定符,分别是: public、private、protected。
- 成员都默认为 public, 被此限定符修饰的成员是可以被外部访问。
- 当成员被设置为 private 之后, 被此限定符修饰的成员是只可以被类的内部访问。
- 当成员被设置为 protected 之后, 被此限定符修饰的成员是只可以被类的内部以及类的子类访问。
class 可以作为接口
当需要我们设置defaultProps初始值的时候,我们只需要:
class 起到了接口和设置初始值两个作用,方便统一管理,减少了代码量。
函数Function
重载
场景:比如有个函数实际上只接受1、2、4个参数,但是如果我调用并传入三个参数,是不会报错的,这就是类型的不安全。为了解决上述问题,因此函数重载出现了。
函数重载在多人协作项目或者开源库中使用非常频繁,因为一个函数可能会被大量的开发者调用,如果不使用函数重载,那么会造成额外的麻烦。虽然在普通的开发中使用到这个功能的几率并不大,但是一旦涉及多人使用的库相关开发,函数重载可谓是必不可少的利器。
泛型(generic)的妙用
泛型给予开发者创造灵活、可重用代码的能力
对未知参数的类型,我们需要兼容各个类型,所以我们需要变量,这个变量代表了传入的类型,然后再返回这个变量,它是一种特殊的变量,只用于表示类型而不是值,这个类型变量在 TypeScript 中就叫做「泛型」。
function returnItem<T>(para: T): T{
return para
}
多个类型参数
function swap<T, U>(tuple: [T, U]): [U, T]{
return [tuple[1], tuple[0]];
}
泛型变量
如果我们一开始就知道T是数组,那我们可以直接用Array来代替T,在这里泛型变量 T 当做类型的一部分使用,而不是整个类型,增加了灵活性。
泛型接口
interface ReturnItemFn<T> {
(para: T): T
}
const returnItem: ReturnItemFn<number> = (para) => para
泛型类
// 比如一个栈class类:这样的写法,无论是number还是string都可以通用。
class Stack<T> {
private arr: T[] = []
public push(item: T) {
this.arr.push(item)
}
public pop(item: T) {
this.arr.pop()
}
}
泛型约束与索引类型
const params = string | number
class Stack {...}
除了string与number其他的类型都会报错
索引类型:,索引类型 keyof T 把传入的对象的属性类型取出生成一个联合类型,这里的泛型 U 被约束在这个联合类型中,这样一来函数就被完整定义了。
function getValue<T extends obj, U extends keyof T>(obj: T, key: U) {return obj[key];} // ok
const a = {
name: 'asdfasdf'
id: 223322
}
// 这个时候 getValue 第二个参数 key 的类型被约束为一个联合类型 name|id,他只可能是这两个之一
getValue(a, ) // name | id
使用多重类型进行泛型约束
如果一个泛型别约束,只被允许实现以下两个接口的类型
此时以下都是错误的方法:
正确的方法是将接口 FirstInterface与SecondInterface 作为超接口来解决问题:
或者class Demo<T extends FirstInterface & SecondInterface>
这样就可以使用并不会报错
二、高级用法
交叉类型方法例子
交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。
在 JavaScript 中,混入是一种非常常见的模式,在这种模式中,你可以从两个对象中创建一个新对象,新对象会拥有着两个对象所有的功能。
interface IAnyObject {
[prop: string]: any
}
function minix<T extends IAnyObject, U extends IAnyObject>(first: T, second: U) {
const result = <T & U>{};
for (let id in first) {
(<T>result)[id] = first[id]
}
for (let id in second) {
if (!(result.hasOwnProperty(id))) {
(<U>result)[id] = second[id]
}
}
return result
}
const obj = minix({ a: 'min' }, { b: 3 })
// 现在 x 拥有了 a 属性与 b 属性
const a = x.a;
const b = x.b;
联合类型与类型别名
联合类型:它使用 | 作为标记,如 string | number类似或语句。
类型别名:
type some = string | number,some就是类型别名。
类型别名也可以是泛型,在内部属性也可引用自己,例如:
type Tree = {
value: T,
left: Tree,
right: Tree
}
字面量类型
与联合类型混合使用:
类型字面量
type Foo = {
baz: [
number,
'xiaoxiao'
];
toString(): string;
readonly [Symbol.iterator]: 'github';
0x1: 'foo';
'bar': 12n;
}
类型别名type与interface很像,但是存在区别:(必考点)
- interface 只能用于定义对象类型,而 type 的声明方式除了对象之外还可以定义交叉、联合、原始类型等,类型声明的方式适用范围显然更加广泛
- interface可以实现接口的 extends 和 implements
- interface 可以实现接口合并声明
- 接口创建了一个新的名字,可以在其它任何地方使用,类型别名并不创建新名字,比如:错误信息就不会使用别名。
可辨识联合类型
我们先假设一个场景,现在又两个功能,一个是创建用户即 create,一个是删除用户即 delete.
如果不需要id怎么根据接口写代码?
如何区分有没有id?
我们上面提到了 userAction.action 就是辨识的关键,被称为可辨识的标签,我们发现上面这种模式要想实现必须要三个要素:
- 具有普通的单例类型属性—可辨识的特征,上文中就是
delete与create两个有唯一性的字符串字面量 - 一个类型别名包含联合类型
- 类型守卫的特性,比如我们必须用
ifswitch来判断userAction.action是属于哪个类型作用域即delete与create
装饰器(decorator)
装饰器(decorator)最早在 Python 中被引入,它的主要作用是给一个已有的方法或类扩展一些新的行为,而不是去直接修改它本身.在 ES2015 进入 Class 之后,当我们需要在多个不同的类之间共享或者扩展一些方法或行为的时候,代码会变得错综复杂,极其不优雅,这也就是装饰器被提出的一个很重要的原因.(现在还处于草案阶段)。
所以在 JavaScript 中我们需要 Babel 插件 babel-plugin-transform-decorators-legacy 来支持 decorator,而在 Typescript 中我们需要在 tsconfig.json 里面开启支持选项 experimentalDecorators.
明确两个概念:
- 目前装饰器本质上是一个函数,
@expression的形式其实是一个语法糖, expression 求值后必须也是一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入. - JavaScript 中的 Class 其实也是一个语法糖
赋值断言、is 关键字、可调用类型注解和类型推导
明确赋值断言:明确赋值断言是一项功能,它允许将!放置在实例属性和变量声明之后,来表明此属性已经确定它已经被赋值了
is 为关键字的「类型谓语」把参数的类型范围缩小了,当使用了 test is string 之后,我们通过 isString(foo) === true 明确知道其中的参数是 string
索引类型查询操作符
keyof,即索引类型查询操作符,我们可以用 keyof 作用于泛型 T 上来获取泛型 T 上的所有 public 属性名构成联合类型。
举个例子,有一个 Images 类,包含 src 和 alt 两个 public 属性,我们用 keyof 取属性名:
索引访问操作符
我们已经取出属性名 propsNames ,然后通过类型访问的操作符来获取值的类型
映射类型
现在有一个需求是把User接口中的成员全部变成可选的,我们应该怎么做?难道要重新一个个:前面加上?
这个时候映射类型就派上用场了,映射类型的语法是[K in Keys]:
- K:类型变量,依次绑定到每个属性上,对应每个属性名的类型
- Keys:字符串字面量构成的联合类型,表示一组属性名(的类型)
条件类型
若 T 能够赋值给 U,那么类型是 X,否则为 Y,有点类似于JavaScript中的三元条件运算符.
条件类型如果使用必须有两个要求
- 第一是分布式有条件类型:分布式有条件类型在实例化时会自动分发成联合类型。
- 第二是条件类型里待检查的类型必须是naked type parameter: 指的是裸类型参数,就是类型参数没有被包装在其他类型里,比如没有被数组、元组、函数、promise等等包装。
当我们给类型NakedUsage加入联合类型number | boolean时,它的结果返回"NO" | "YES",相当于联合类型中的number和boolean分别赋予了NakedUsage然后再返回出一个联合类型。这个操作类比Array.map();
例子:如何设计一个类型工具Diff<T, U>?我们要找出T类型中U不包含的部分:
never类型表示不会是任何值,即什么都没有,甚至不是null类型
infer关键字
infer 是工具类型和底层库中非常常用的关键字,表示在 extends 条件语句中待推断的类型变量。 juejin.im/book/5da087…
常用工具类型解读
一切类型工具的基础就是泛型。
+ -这两个关键字用于映射类型中给属性添加修饰符,比如-?就代表将可选属性变为必选,-readonly代表将只读属性变为非只读.
比如TS就内置了一个类型工具Required<T>,它的作用是将传入的属性变为必选项:
Exclude 的作用是从 T 中排除出 可分配 给 U的元素.
Omit<T, K> 的作用是忽略T中的某些属性.
Merge<O1, O2> 的作用是将两个对象的属性合并:
Compute的作用是将交叉类型合并
结果是:
type R1 = {
x: "x",
y: "y"
}
Record 允许从 Union 类型中创建新类型,Union 类型中的值用作新类型的属性
编写 d.ts 文件
关键字 declare 表示声明的意思,我们可以用它来做出各种声明:
- declare var 声明全局变量
- declare function 声明全局方法
- declare class 声明全局类
- declare enum 声明全局枚举类型
- declare namespace 声明(含有子属性的)全局对象
- interface 和 type 声明全局类型