前置知识
Any 任意类型
定义为 any 类型是可以赋值给任意类型,当类型转换遇到困难或者数据结构复杂难以定义可以使用 any。any 给了我们很大的自由,如果所有类型定义为any,TypeScript 其实就失去的它存在的意义了。
any 类型太宽松了。很多场景会编写出类型正确编译通过,运行时报错的代码。例如
let cc: any;cc.trim();new cc();cc.cc.eat(); // 完全可以通过编译,但是运行明显是会报错
Unknown 类型
在 TypeScript 中,当我们不确定一个类型是什么类型,可以选择给其声明为 any 或者 unknown。但实际上,TypeScript 更推荐使用 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 就不一样了,它需要先进行断言或者类型守卫后才能通过编译
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 为什么是安全的了。它只是指明了类型还未确认,后续还需要去断言,也就是它并未放弃类型检查。
类型推论
有时候没有明确指明类型,但是 TypeScript 还是能帮我们推断出一个类型
let count = 100 // ts 推断出 count 是一个 number 类型count = '100' // 编译报错
有一点要特别注意的是,当在声明变量的时候没有给变量赋值时,该变量会被推断成 any 类型。从而跳过类型检查。
let xx = 1x = '1'// 这段代码完全 ok 的。 编译时不会报错
联合类型
联合类型表示取值可以为多种类型中的某一种。
let money: number | stringmoney = 10000money = '1w'
当 TypeScript 还不知道联合类型变量到底是哪一种变量时候,那么我们只能访问该联合类型共有的属性和方法。
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))}
一方面不能滥用 as any,另一方面也不要完全否定它的作用,我们需要在类型的严格性和开发的便利性之间掌握平衡(这也是 TypeScript 的设计理念之一),才能发挥出 TypeScript 最大的价值。
类型别名
类型别名用来给一个类型起个新名字。 关键词 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)时,接口和别名只作为类型定义时,两者皆可。
泛型正文
软件工程中,我们不仅要创建一致的定义良好的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
