lei4519 / blog

记录、分享
4 stars 1 forks source link

TS 类型编程 #60

Open lei4519 opened 7 months ago

lei4519 commented 7 months ago

现如今 TS 已经完全走进了前端社区,纵观流行的前端框架/库,都有 TS 类型支持,不少框架/库更是对于源码是使用 TypeScript 开发,完美支持 TypeScript 类型提示当作一大亮点去宣传。

在业务中用 TS 写写页面、表格等上层逻辑,基本就是把后端数据定义/转换成 interface,往后一把梭就完了,不需要对 TS 的类型系统有深入的了解。

但当封装公用组件/函数时,如果想封装的代码能够支持 TS 类型,就需要了解类型编程的一些技巧,本文就分享一些常用的类型编程技巧。

好的 TS 类型支持

良好的类型推断,避免重复的类型定义

// string[]  
const result = [1, 2, 3]  
  .map((item) => () => item)  
  .map((fn) => ({ value: fn() }))  
  .map((item) => ({ label: `${item.value}` }))  
  .map((item) => item.label)  

// ---  
interface ResponseData {  
  is_success: boolean  
  data: {  
    id: number  
    pu_info: Record<string, string>  
    service_price: number  
    order_price: number  
  }[]  
}  

const fetchListData = async () => {  
  let data = await request<ResponseData>("/api/list") // ResponseData  

  data = transformKeys(data) // ResponseData1: isSuccess puInfo orderPrice  

  return transformMoney(data, ["orderPrice"]) // ResponseData2: orderPrice: string  
}  

const { data, isLoading, isError } = useSWR(fetchListData) // data: ResponseData2  

const id = get(data, "data.id") // number  

常见套路:提取 → 转换 → 重组 → 递归(循环)

前置知识

基础知识

// 类型  
type T = string | number | boolean | class | interface // ...  

// 类型推断  
{  
  let A = 1 // number  
  let B = "hello" // string  
  let C = [1, 2, 3] // number[]  
}  

// 常量类型推断  
{  
  const A = 1 // 1  
  const B = "hello" // 'hello'  
  const C = [1, 2, 3] as const // readonly [1, 2, 3]  
}  

// 联合类型:|  
type Union = { hello: string } | { world: number }  

// 交叉类型: &  
type Intersect = { hello: string } & { world: number }  

// .....  

对象操作:keyof、in

// 返回对象 key 的常量联合类型  
keyof T // hello | world  

// 返回对象值的联合类型  
T[keyof T] // string | number  
T['hello'] // string  
T['world'] // number  

// in 操作符遍历联合类型  
interface A {  
    [K in string | number]: never  
    [K in 'hello' | 'world']: never  
    [K in keyof T]: T[K]  
}  

// ------ 源码 ------  

/**  
 * Make all properties in T required  
 */  
type Required<T> = {  
    [P in keyof T]-?: T[P];  
};  

/**  
 * From T, pick a set of properties whose keys are in the union K  
 */  
type Pick<T, K extends keyof T> = {  
    [P in K]: T[P];  
};  

/**  
 * Construct a type with a set of properties K of type T  
 */  
type Record<K extends keyof any, T> = {  
    [P in K]: T;  
}  

类型约束、条件类型:extends

// 泛型参数约束  
type T<P extends string> = {}  

// 条件类型  
type IsString<T> = T extends string ? true : false  
type A = IsString<"1"> // true  
type B = IsString<2> // false  

条件类型推断:infer

  1. 声明一个类型
  2. 如果类型约束成功
  3. 将模式匹配后的类型分配给 1
// 条件类型推断  
type Example<T> = T extends infer R ? R : never  
type A = Example<"1"> // '1'  
type B = Example<2> // 2  

模式匹配

type A = [1, 2, 3]  
type ExampleA = A extends [infer First, ...infer Rest] ? First : never // 1  

type B = "123"  
type ExampleB = B extends `${infer FirstChar}${infer Rest}` ? FirstChar : never // '1'  

type C = (p: number) => string  
type ExampleB = C extends (p: infer R) => any ? R : never // number  

// ------ 源码(删减了类型约束) ------  

/**  
 * Obtain the parameters of a function type in a tuple  
 */  
type Parameters<T> = T extends (...args: infer P) => any ? P : never  

/**  
 * Obtain the return type of a function type  
 */  
type ReturnType<T> = T extends (...args: any) => infer R ? R : any  

/**  
 * Obtain the parameters of a constructor function type in a tuple  
 */  
type ConstructorParameters<T> = T extends abstract new (...args: infer P) => any  
  ? P  
  : never  

/**  
 * Obtain the return type of a constructor function type  
 */  
type InstanceType<T> = T extends abstract new (...args: any) => infer R  
  ? R  
  : any  

函数参数泛型推导

function f<A, B, C extends string>(a: A, b: B, c: C) {}  

f("a", "b" as const, "c") // function f<string, "b", "c">(): void  

interface Api {  
  "/api/phone": { phone: string }  
  "/api/email": { email: string }  
}  

function request<T extends keyof Api>(url: T): Api[T] {  
  return "" as any  
}  

request("/api/phone") // { phone: string }  
request("/api/email") // { email: string }  

示例

Get

Infer 在字符串模板中的小细节

  1. 如果模版中只有一个 infer,它会尽可能多的匹配(贪婪模式)。比如用 ${infer T}x 去匹配 'abcxxx',T 为 'abcxx'。
  2. 如果有多个 infer,最后一个 infer 是贪婪模式,前面的是非贪婪模式。比如 ${infer A}${infer B}${infer C} 去匹配 'abcdefg',结果为:A: 'a',B: 'b',C: 'cdefg'
type Get<T, Paths extends string> = Paths extends keyof T // 递归出口  
  ? T[Paths]  
  : Paths extends `${infer K}.${infer R}` // 匹配 . 语法  
  ? K extends keyof T  
    ? Get<T[K], R> // 递归  
    : unknown  
  : unknown  

// _.get(obj, 'a.b.c')  
type R = Get<{ a: { b: { c: { d: number } } } }, "a.b.c">  

RTK Query

Api 设计

// @ts-nocheck  
// Define a service using a base URL and expected endpoints  
export const pokemonApi = createApi({  
  endpoints: (builder) => ({  
    getPokemonByName: builder.query<Pokemon[], string>({  
      query: (name) => `pokemon/${name}`,  
    }),  
    getPokemonById: builder.query<Pokemon, string>({  
      query: (id) => `pokemon/${id}`,  
    }),  
  }),  
})  

// Export hooks for usage in functional components, which are  
// auto-generated based on the defined endpoints  
export const { useGetPokemonByNameQuery, useGetPokemonByIdQuery } = pokemonApi  

const { data, isLoading, isError } = useGetPokemonByNameQuery()  

实现

interface Endpoints {  
  getPokemonByName: string  
  getPokemonById: number  
}  

// 转化 key 字符串  
// 问题:无法取出准确的值类型  
type _B = {  
  [K in `use${Capitalize<keyof Endpoints>}Query`]: Endpoints[keyof Endpoints]  
}  

// 准确的值类型:双重遍历  
// 问题:结构不是我们想要的  
type B = {  
  [P in keyof Endpoints]: {  
    [K in `use${Capitalize<P>}Query`]: Endpoints[P]  
  }  
}  

// 取出值类型  
// 问题:联合类型,无法使用  
type C = B[keyof B]  

// 联合类型转交叉类型  
type UnionToIntersect<T> = (T extends any ? (k: T) => any : never) extends (  
  k: infer P  
) => any  
  ? P  
  : never  

type E = UnionToIntersect<C>  

const e = {} as E  

e.useGetPokemonByNameQuery // ✅  

Redux

Api 设计

import { configureStore, createSlice, PayloadAction } from "@reduxjs/toolkit"  

export const counterSlice = createSlice({  
  name: "counter",  
  initialState: {  
    value: 0,  
  },  
  reducers: {  
    increment: (state) => {  
      state.value += 1  
    },  
    incrementByAmount: (state, action: PayloadAction<number>) => {  
      state.value += action.payload  
    },  
  },  
})  

export default configureStore({  
  reducer: {  
    counter: counterSlice.reducer,  
  },  
})  

// @ts-ignore  
dispatch({ type: "counter/incrementByAmount", payload: 1 })  

实现:略

TransformKeys

interface ResponseData {  
  is_success: boolean  
  data: {  
    id: number  
    pu_info: Record<string, string>  
    service_price: number  
    order_price: number  
  }[]  
}  

// 下划线转驼峰  
type Camelize<  
  S extends string,  
  V extends string = ""  
> = S extends `${infer A}_${infer B}${infer C}`  
  ? Camelize<C, `${V}${A}${Capitalize<B>}`>  
  : `${V}${S}`  

type A = Camelize<"hello_java_script">  

// 工具函数:取出 Value 的类型  
type ValueType<T> = T[keyof T]  

// 联合转交叉  
type UnionToIntersect<T> = (T extends any ? (k: T) => any : never) extends (  
  k: infer P  
) => any  
  ? P  
  : never  

// 转换 keys  
type TransformKeys<T> = UnionToIntersect<  
  ValueType<{  
    [K in keyof T]: K extends string  
      ? {  
          [P in `${Camelize<K>}`]: T[K] extends any[]  
            ? Array<TransformKeys<T[K][0]>>  
            : T[K] extends Record<string, any>  
            ? TransformKeys<T[K]>  
            : T[K]  
        }  
      : never  
  }>  
>  

type R1 = TransformKeys<ResponseData>  

const a = {} as R1  

a.data[0].id  

TransformMoney

transformMoney(data, ["orderPrice"]) // ResponseData2: orderPrice: string  

// 元祖转联合  
type A = ["A", "B"]  

type B = A[number] // A | B  

// ---  

interface Data {  
  id: number  
  servicePrice: number  
  orderPrice: number  
}  

// 转换 keys  
type Transform<T, Keys> = {  
  [K in keyof T]: K extends Keys ? string : T[K]  
}  

type R = Transform<Data, "orderPrice">  

扩展知识

字符串类型处理

/**  
 * Convert string literal type to uppercase  
 */  
type Uppercase<S extends string> = intrinsic  

/**  
 * Convert string literal type to lowercase  
 */  
type Lowercase<S extends string> = intrinsic  

/**  
 * Convert first character of string literal type to uppercase  
 */  
type Capitalize<S extends string> = intrinsic  

/**  
 * Convert first character of string literal type to lowercase  
 */  
type Uncapitalize<S extends string> = intrinsic  

联合类型转交叉类型

函数参数逆变   + 分配条件类型

// 类型推断时,在逆变位置的同一类型变量中的多个候选会被推断成交叉类型(函数签名重载的参数位置类型会被推断为交叉类型)  
// https://github.com/Microsoft/TypeScript/pull/21496  
type Func = ((arg: { a: string }) => void) | ((arg: { b: number }) => void)  
type U = Func extends (arg: infer A) => any ? A : never // { a: string } & { b: number }  

// {a: string} | {b: number} 如何变为函数签名重载?  

// 分配条件类型  
// 官网:当泛型类型是联合类型时,条件判断会变得具有分配性  
// 人话:extends 左边的范型联合类型,会拆开分别处理  
type ToArray<Type> = Type extends any ? Type[] : never  
type StrArrOrNumArr = ToArray<string | number> // string[] | number[] 而非是 (string | number)[]  

逆变、协变

类型系统中的概念,表达父子类型关系

TS 中参数位置是逆变的,返回值是协变的

逆变协变.excalidraw

Kanban--2024-04-14_16.41.24-6.png

https://github.com/sl1673495/blogs/issues/54

https://jkchao.github.io/typescript-book-chinese/tips/covarianceAndContravariance.html

元祖转联合

type A = ["A", "B"]  

type B = A[number]  

类型体操

TypeScript 类型体操天花板,用类型运算写一个 Lisp 解释器

用 TypeScript 类型运算实现一个中国象棋程序

Type Challenges