学好ts的三步:
- 基本的类型注解
- 深入语法,会封装工具类型或者说类型体操
- 工程化配置
本文用于记录、总结学好ts的第一步:基本的类型注解,文章中有什么问题也欢迎大佬指正。
一:基础类型
Object object {}
- Object为原型链的底层,是所有对象、函数(也是对象)的父级。装箱对象例如Number是number的父级,所以Object类型包含了所有类型。
- 正因为Object的特性,专门提供了object类型,它表示数组、对象、函数类型。
- {}可以理解为new Object()实例,所以它跟Object的类型范围一致。
string number boolean symbol
undefined null
- 在ts中是有意义的类型,你不去声明不会平白无故出现undefined null的。
- 例如 interface aa { name?: string } 从js的惯性思维理解,name这里相当于name: string | undefined,而实际上上述类型的效果应该为{} | {name: string}。
void
- 表示函数没有显示返回值。
- 如果函数显示return但没有值,那么函数的返回值类型应该精确为undefined。
对象类型
- 使用interface定义对象对外的接口,也可以理解为描述对象的结构
- 接口的合并,interface 声明的合并,不能覆盖、只能拓展
- 接口的继承,如果子接口中有父接口中同名属性,那么子接口中的同名属性,那么就需要:子属 兼容 父属
- 操作修饰符readonly
数组类型
- 获取数组长度arrType['length'];访问数据类型对应位置的类型attType[0];
- 元组:类型与位置强关联
字面量类型
枚举enum
- 枚举的原理
enum Color {
black,
red,
blue,
}
var items;
(function (items) {
items[items['black' = 0]] = 'black';
items[items['red' = 1]] = 'red'
items[items['blue' = 2]] = 'blue'
})(items || (items = {}))
- 与普通对象的区别
- 双向映射
- 值需要为数字才会双向映射
- 常量枚举:编译后啥也没有
函数、class
- 主要需要考虑的是 入参、结果类型
- 函数重载,定义顺序最好和实现的逻辑顺序一致
- 协变与逆变,参数类型逆变,结果类型 协变。一个函数类型的子类型,它的参数可以更少更精简,而且结果类型必须要满足我之前的类型
- class
- 属性和函数的访问符(public protect private)、操作符(readonly)
- 抽象类abstract ,抽象类的属性、方法如果需要实现的,也需要abstract 修饰。需要通过其他类来实现这个抽象类,class Other implements Absxx。抽象类的本质就是描述类的结构,也就是和interface一样的作用,所以使用接口也可以替代抽象类的功能,不过语义就没有抽象类好
二:any unkown never 类型断言
- any:当一个变量类型为any时,这个变量的操作就跳过了类型推断与类型校验。在兼容性判断时,例如 a extends b, 如果泛型a为any,那可以理解为a 既可以是b,也可以不是b
- unknown:top type类型
- never: 虚无类型,或者说正常不应该到达的类型
- 类型断言as
- 如果两个类型不存在公共父类型,那么需要双重断言
- 非空断言!,例如obj.fn!() 意思是:!前面的声明一定是非空(排除undefined null 类型)。非空断言其实就是as断言的简写,所有断言都是ts类型层面,如果js运行环境中的值与断言的类型不一样,可能就报错
三:类型(的)工具
类型创建
- type:定义一组类型便于复用,也叫类型别名,不可以重复定义。
- 联合类型 type C = a | b; 既可以是a也可以是b。
- 交叉类型,type c = a && b; 那么c的类型就又要满足a,又要满足b;如果a和b都是联合类型,那么就相当于取交集。
- 索引类型,快速声明一个键值类型一致的结构,{ [name: 类型]: 类型b }
- 索引类型查询 keyof 类型,得到一个键的联合类型
- 索引的类型访问,type1 [keyType]。这里的keyType可以是单个字面量类型,也可是该对象类型键类型的集合,下面的例子:
- 已知属性的对象类型,objType['a' | 'b']
- 索引签名类型,objType[string]
- 工具类型,type PickValue = T[keyof T]
- 映射类型in, 索引签名的好搭档,将一个联合类型当作索引签名类型中的键类型
类型查询
- typeof,它可以推导出变量的类型,并且是最窄的推导程度。
- ReturnType, 返回函数类型的返回值类型
type ReturnType1<T extends (...args: any) => any> = T extends (
...args: any) => infer R
? R
: never;
类型安全保护
- 理解:ts会随着你的代码逻辑,不断尝试收窄联合类型,不过这个能力还不够强,只有部分场景。首先需要一个判断环境,if 或者switch case,其次是特性的js判断逻辑:
- typeof,变量的类型是由原始类型组成的联合类型,例如string | number。if( type foo === 'string' ) {}。
- in,变量的类型是由对象类型组成的联合类型,可以通过判断键是不是独享属性来收窄类型,
- intanceof
- 对于同名属性但值是不同类型,只能通过字面量来区分
interface Foo1 {
kind: "foo";
diffType: string;
fooOnly: boolean;
shared: number;
}
interface Bar {
kind: "bar";
diffType: number;
barOnly: boolean;
shared: number;
}
function handle1(input: Foo1 | Bar) {
if (input.kind === "foo") {
input.fooOnly;
} else {
input.barOnly;
}
}
5. ts是无法跨越上下文来进行类型收集,所以如果将判断逻辑提取到其他函数内,那么还要结合is关键字,也就是类型守卫。例如function isString(target: unkown) target is string { return 当返回值为true时,target是string类型}
四:泛型
-
如果说 TypeScript 是一门对类型进行编程的语言,那么泛型就是这门语言里的(函数)参数
-
泛型约束extends 默认值。 A extends B 意味着 A 是 B 的子类型,也就是说 A 比 B 的类型更精确,或者说更复杂。
- 联合类型子集均为联合类型的子类型,即 1、 1 | 2 是 1 | 2 | 3 | 4 的子类型。
- 字面量类型是对应原始类型的子类型,即 'linbudu' extends string,599 extends number 成立。
- { name: string } 是 {} 的子类型,因为在 {} 的基础上增加了额外的类型,基类与派生类(父类与子类)同理。
- 泛型约束和条件类型,type c<T extends string | number> = T extends string ? 'string' : T extends number ? 'number' : never。
- 存在泛型约束和条件类型两个 extends 可能会让你感到疑惑,但它们产生作用的时机完全不同,泛型约束要求你传入符合结构的类型参数,相当于参数校验。而条件类型使用类型参数进行条件判断(就像 if else),相当于实际内部逻辑。
- 多个泛型参数存在关联
type ProcessInput<
Input,
SecondInput extends Input = Input,
ThirdInput extends Input = SecondInput
> = number;
- 类型别名中的泛型,主要是通过手动传入
- 函数中的泛型,会自动将类型 提取给泛型,可以给全部,function handle(input: T): T {} ;也可以给部分 function handle(input: { name: T }): T {}
function universalAdd<T extends number |
bigint | string>(x: T, y: T): T {
return x + (y as any);
}
universalAdd(1, 2) // 1 | 2
universalAdd(1, 'a')// 类型不允许
函数的泛型可以通过主动传入,更多的是通过函数隐式推导出来的,universalAdd这个例子,泛型参数T可以是number string bigint的子类型,当传入第一个参数数字字面量1时,就限定了类型T的原始类型为number,所以第二个参数只能时数字,而类型T又要同时满足1 和 2,所以最终泛型T被推导为1 | 2
- class中的泛型和函数基本类似
五:类型系统
- 结构化类型系统,基于类型结构判断兼容性,和名字无关。ts就属于结构化类型系统
- 标称类型系统,基于类型名称判断兼容性
六:兼容性判断
- 判断方式
- 条件类型来判断,type Result = 'pandu' extends string ? 1 : 2;
- 赋值,如果变量 a = b成功 ;那么变量b的类型是变量a的子类型,换种说法 b兼容a
- 通过条件类型判断,可以直接使用参数去判断,也可以使用泛型。
- type a = 类型参数 extends 判断条件的类型 ? true : false
- type a< T> = T extends 判断条件的类型 ? true : false
- 条件类型与infer
- 只有条件类型中,才可以使用infer提取部分类型信息
- 如果限制了类型参数,在使用infer的时候,可能会丢失原来被限制过的类型信息
type ReverseKeyValue<T extends Record<string, string>> =
T extends Record<infer K,infer V>
? Record<V, K>
: never;
// 类型“V”不满足约束“string | number | symbol”
// 上面的反转key和value是整个转的,下面是更具体的版本
type ReverseKeyValue<T extends Record<string, string>> =
T extends Record<infer K, infer V>
? {[name in T[keyof T]]: PickKeyByValue<T, name & string>}
: never;
type PickKeyByValue<T extends Record<string, string>, V extends string> = {
[name in keyof T]: T[name] extends V ? name : never
}[keyof T]
type obj = {
name: 'nameV';
age: 'ageV';
}
type rObj = ReverseKeyValue<obj>;// {nameV: "name"; ageV: "age";}
上面的例子中,因为typeScript 中这样对键值类型进行 infer 推导,将导致类型信息丢失,而不满足索引签名类型只允许 string | number | symbol 的要求。
- 约束infer
type FirstType<T> = T extends [infer F extends string,
...reset: unknown[] ] ? F : never
- 分布式条件类型:条件类型在满足一定情况下会执行的逻辑
- 条件:类型参数通过泛型传入 & 泛型参数是联合类型 & 泛型参数在条件类型中完全裸露(也就是直接作为判断参数)。如type test < T> = T extends 条件类型 ? 'y' : 'n'。
// 完全裸露这个条件如何破坏?
// 类型参数包裹数组
// 满足:
type test<T> = T extends string | number ? 1 : 2;
type a = test<1 | 2>
// 不满足:
type test<T> = [ T ] extends string | number ? 1 : 2;
type a = test<1 | 2>
// 再包裹泛型也可以破坏这个条件
type test<T> = test2<T> extends 1 | 2 ? 'y' : 'n'
2. 如果满足上述条件,即将这个联合类型拆开来,每个分支分别进行一次条件类型判断,再将最后的结果合并起来。最终会形成交集的效果:
// 示例:
type test<T> = T extends 1 | 2 | 3? 'y' : 'n';
type a = test<1 | 4>; // 'y' | 'n'
- any 在作为判断参数时(不管是否裸露),只要此时判断条件类型不是any,都会产生分布式条件类型的效果
// 直接使用,返回联合类型
type Tmp1 = any extends string ? 1 : 2; // 1 | 2
type Tmp2<T> = T extends string ? 1 : 2;
// 通过泛型参数传入,同样返回联合类型
type Tmp2Res = Tmp2<any>; // 1 | 2
// 如果判断条件是 any,那么仍然会进行判断
type Special1 = any extends any ? 1 : 2; // 1
type Special2<T> = T extends any ? 1 : 2;
type Special2Res = Special2<any>; // 1
- never仅在通过泛型参数传入且作为判断参数且裸露时,会直接返回never
// 直接使用,仍然会进行判断
type Tmp3 = never extends string ? 1 : 2; // 1
type Tmp4<T> = T extends string ? 1 : 2;
// 通过泛型参数传入,会跳过判断
type Tmp4Res = Tmp4<never>; // never
// 如果判断条件是 never,还是仅在作为泛型参数时才跳过判断
type Special3 = never extends never ? 1 : 2; // 1
type Special4<T> = T extends never ? 1 : 2;
type Special4Res = Special4<never>; // never
- 判断never类型
type isNever<T> = [T] extends [never] ? true : false
- 判断any类型,需要利用any和其他类型交叉还会得到any的特性
type isAny<T> = 0 extends 1 & T ? true : false
- 判断unknown类型
type isUnknown<T> = unknown extends T ? isAny<T> extends true ? false : true : false
七:类型层级顺序
- any unknown
- Object {}
- 装箱类型Number String Boolean
- 基础类型string number boolean undefined null symbol
- 对应的字面量类型
- never