TypeScript类型体操5

类型编程主要的目的就是对类型做各种转换,如何对类型做修改?
TypeScript 类型系统支持 3 种可以声明任意类型的变量: type、infer、类型参数。

  • type:类型别名,声明一个变量存储某个类型。type t = Promise<number>
  • infer:用于提取类型,存到声明的变量里。type GetValueType<P> = P extends Promise<infer Value> ? Value : never;
  • 类型参数:用于接受具体类型。type isTwo<T> = T extends 2 ? true: false;

但是上述三种’变量’不是通常我们理解的变量,因为不能被重新赋值。但是做类型编程是为了能够产生各种负责的类型,因此只有通过重新构造来产生新类型。

重新构造

TypeScript 的 type、infer、类型参数声明的变量都不能修改,想对类型做各种变换产生新的类型就需要重新构造。

数组类型重新构造

添加类型

当我们声明了一个元组变量后,需要给这个元组类型添加新类型时:

//在最后添加 push
type tuple = [1,2,3];

type Push<Arr extends unknown[], Ele> = [...Arr, Ele];

type result = Push<[1,2,3],4>//[1,2,3,4]

//在最前面添加 unshift

type Unshift<Arr extends unknown[], Ele> = [Ele, ...Arr];

type result2 = Unshift<[1,2,3],0>

复杂案例
构造要求:
将已有的两个元组类型合并成一个新的元组类型

type tuple1 = [1,2];
type tuple2 = ['hello','world'];

//合并后
type tuple = [[1,'hello'],[2,'world']];

思路:提取元组中的两个元素,构造成新的元组

type Zip<One extends [unknown,unknown], Other extends [unknown,unknown]> = 
    One extends [infer OenFirst, infer OneSecond] 
        ? Other extends [infer OtherFirst, infer OtherSecond]
            ? [[OneFirst, OtherFirst],[OneSecond, OtherSecond]] : []
            : [];

type result = Zip<tuple1,tuple2>//[[1,'hello'],[2,'world']]
  • 两个类型参数 One、Other 是两个元组,类型是 [unknown, unknown],代表 2 个任意类型的元素构成的元组
  • 通过 infer 分别提取 One 和 Other 的元素到 infer 声明的局部变量 OneFirst、OneSecond、OtherFirst、OtherSecond 里
  • 用提取的元素构造成新的元组返回

同理,尝试一下合并任意各元素的元组,使用递归的方式:

    type Zip2<One extends unknown[], Other extends unknown[]> = 
        One extends [infer OneFirst, ...infer OneRest]
            ? Other extends [infer OtherFirst, ...infer OtherRest]
                ? [[OneFirst,OtherFirst], ...Zip2<OneRest,OtherRest>] : []
                    : [];
  • 类型参数 One、Other 声明为 unknown[],也就是元素个数任意,类型任意的数组
  • 每次提取 One 和 Other 的第一个元素 OneFirst、OtherFirst,剩余的放到 OneRest、OtherRest 里
  • 用 OneFirst、OtherFirst 构造成新的元组的一个元素,剩余元素继续递归处理 OneRest、OtherRes

字符串类型的重新构造

思路:从已有字符串类型中提取部分字符串,经过一系列变换,构造成新的字符串类型

CapitalizeStr
将一个字符串字面量类型的’hello’转为首字母大写的’Hello’:

type CapitalizeStr<Str extends string> = Str extends `${infer First}${infer Rest}` ? `${Uppercase<First>}${Rest}` : Str;

type result = CapitalizeStr<'hello'>
  • 声明了类型参数 Str 是要处理的字符串类型,通过 extends 约束为 string
  • 通过 infer 提取出首个字符到局部变量 First,提取后面的字符到局部变量 Rest
  • 使用 TypeScript 提供的内置高级类型 Uppercase 把首字母转为大写,加上 Rest,构造成新的字符串类型返回

CamelCase
将一个how_are_you重新构造为howAreYou:

type CamelCase<Str extends string> = Str extends `${infer Left}_${infer Right}${infer Rest}` ? `${Left}${Uppercase<Right>}${CamelCase<Rest>}` : Str;
  • 类型参数 Str 是待处理的字符串类型,约束为 string
  • 提取 _ 之前和之后的两个字符到 infer 声明的局部变量 Left 和 Right,剩下的字符放到 Rest 里
  • 然后把右边的字符 Right 大写,和 Left 构造成新的字符串,剩余的字符 Rest 要继续递归的处理

DropSubStr
删除字符串中的某个子串:

type DropSubStr<Str extends string, SubStr extends string> = Str extends `${infer Prefix}${SubStr}${infer Suffix}` ? DropSubStr<`${Prefix}${Suffix}`,SubStr> : Str;

type result = DropSubStr<'hello,,,,,',','>//hello
  • 类型参数 Str 是待处理的字符串, SubStr 是要删除的字符串,都通过 extends 约束为 string 类型
  • 通过模式匹配提取 SubStr 之前和之后的字符串到 infer 声明的局部变量 Prefix、Suffix 中
  • 如果匹配,那就用 Prefix、Suffix 构造成新的字符串,然后继续递归删除 SubStr。直到不再匹配,也就是没有 SubStr 了

函数类型的重新构造

我们可以提取函数的参数类型和返回值类型,当然可以将提取出的类型做修改后再构造出一个新的函数类型。

AppendArgument
在已有的函数类型上添加一个参数:

type AppendArgument<Func extends Function, Arg> = Func extends (...args: infer Args) => infer ReturnType ? (...args:[...Args,Arg]) => ReturnType : never;

type result = AppendArgument<(name: string) => boolean,number>//(args_0:string, args_1:number) => boolean;
  • 类型参数 Func 是待处理的函数类型,通过 extends 约束为 Function,Arg 是要添加的参数类型
  • 通过模式匹配提取参数到 infer 声明的局部变量 Args 中,提取返回值到局部变量 ReturnType 中
  • 用 Args 数组添加 Arg 构造成新的参数类型,结合 ReturnType 构造成新的函数类型返回

索引类型的重新构造

索引类型是聚合多个元素的类型,类似于class,对象等都是索引类型,对它的修改和构造新类型涉及到了映射类型的语法:

type obj = {
  name: string;
  age: number;
  gender: boolean;
}

type Mapping<Obj extends object> = { 
    [Key in keyof Obj]: Obj[Key]
}

Mapping
映射过程中对value做修改:

type Mapping<Obj extends object> = {
    [Key in keyof Obj]: [Obj[key],Obj[key],Obj[key]]
}

type result = Mapping<{a:1,b:2}>;//{a:[1,1,1],b:[2,2,2]}

UppercaseKey
对Key做修改,使用as:

type UppercaseKey<Obj extends object> = {
    [Key in keyof Obj as Uppercase<Key & string>] : Obj[Key]
}

type result = UppercaseKey<{a:1,b:2}>//{A:1,B:2}

Record
TypeScript 提供了内置的高级类型 Record 来创建索引类型

type Record<K extends string | number | symbol, T> = {[P in K]:T}

  • 指定索引和值的类型分别为 K 和 T,就可以创建一个对应的索引类型

将上面的类型约束修改一下,约束类型参数 Obj 为 key 为 string,值为任意类型的索引类型:

type UppercaseKey<Obj extends Record<string, any>> = {
    [Key in keyof Obj as Uppercase<Key & string>] : Obj[Key]
}

type result = UppercaseKey<{a:1,b:2}>//{A:1,B:2}

ToReadonly
索引类型的索引可以添加 readonly 的修饰符,代表只读,实现给索引类型添加readonly修饰符:

type ToReadonly<T> = {
    readonly [Key in keyof T]: T[Key];
}
  • 通过映射类型构造了新的索引类型,给索引加上了 readonly 的修饰,其余的保持不变

同理,也可以用相同的方式给索引类型加可选修饰符:

type ToPartial<T> = {
    [Key in keyof T]?: T[Key]
}

ToMutable
给索引类型去掉只读修饰符:

type ToMutable<T> = {
    -readonly [Key in keyof T]: T[Key]
}

同理,去掉可选修饰符:

type ToRequired<T> = {
    [Key in keyof T]-?: T[Key]
}

FilterByValueType
在构造新索引类型的时候根据值的类型做过滤:

type FilterByValueType<Obj extends Record<string,any>, ValueType> = {
    [Key in keyof Obj as Obj[Key] extends ValueType ? Key : never] : Obj[Key];
}

interface Person{
    name: string;
    age: number;
    hobby: string[];
}

type result = FilterByValueType<Person, string | number> // {name:string; age: number;}
  • 类型参数 Obj 为要处理的索引类型,通过 extends 约束为索引为 string,值为任意类型的索引类型 Record<string, any>
  • 类型参数 ValueType 为要过滤出的值的类型
  • 构造新的索引类型,索引为 Obj 的索引,也就是 Key in keyof Obj,但要做一些变换,也就是 as 之后的部分
  • 如果原来索引的值 Obj[Key] 是 ValueType 类型,索引依然为之前的索引 Key,否则索引设置为 never,never 的索引会在生成新的索引类型时被去掉
  • 值保持不变,依然为原来索引的值,也就是 Obj[Key]
### TypeScript 类型体操概述 TypeScript 提供了强大的类型系统,允许开发者创建复杂的泛型和条件类型。这些功能通常被称为“类型体操”,因为它们涉及对类型的复杂操作。 #### 条件类型与 `infer` 关键字 当需要从现有类型中提取子类型时,可以使用带有 `infer` 的条件类型。这使得可以从函数返回值或其他结构化数据中抽取特定部分的类型[^4]。 ```typescript // 定义一个通用类型来解析 Promise 返回值的类型 type AsyncReturnType<T> = T extends Promise<infer U> ? U : never; async function fetchData(): Promise<{ data: string }> { return { data: "example" }; } type ResultType = AsyncReturnType<Awaited<ReturnType<typeof fetchData>>>; // 结果为 {data:string} ``` #### 映射类型与索引签名 映射类型能够基于已有接口或类型定义新的类型,并且可以通过修改某些属性来进行扩展或限制。利用 `keyof` 和索引访问类型 (`T[K]`) 可以方便地处理对象内部各个字段之间的关系[^5]。 ```typescript interface Person { name: string; age?: number; } // 创建一个新的只读版本的人类信息类型 type ReadonlyPerson = { readonly [P in keyof Person]: Person[P]; }; const person: ReadonlyPerson = { name: "Alice" }; person.name = "Bob"; // Error: Cannot assign to 'name' because it is a read-only property. ``` #### 泛型约束中的 `extends` 通过给定参数附加额外的信息,如指定其必须满足某个接口的要求,从而增强灵活性并减少错误的可能性。此方法常用于库设计当中,以便更好地支持第三方模块集成[^3]。 ```typescript function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } const user = { id: 1, username: "john_doe" }; console.log(getProperty(user, "username")); // 输出:"john_doe" ``` #### 使用 `as const` 进行更严格的推断 虽然不建议频繁使用显式的类型转换(`as`),但在某些情况下确实有助于提高编译器的理解能力;而 `as const` 则提供了一种更加安全的方式来声明不可变的数据结构[^2]。 ```typescript const roles = ["admin", "editor"] as const; type Role = typeof roles[number]; // "admin" | "editor" let currentRole: Role = "viewer"; // Type '"viewer"' is not assignable to type '"admin" | "editor"' ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值