Skip to content

Latest commit

 

History

History
492 lines (389 loc) · 11.3 KB

3.类型体操实战.md

File metadata and controls

492 lines (389 loc) · 11.3 KB

TypeScript类型体操实战

热身

Chunk

希望实现这样一个类型:

对数组做分组,比如 1、2、3、4、5 的数组,每两个为 1 组,那就可以分为 1、2 和 3、4 以及 5 这三个 Chunk。

type arr = [1, 2, 3, 4, 5];

type Chunk<s
  Arr extends any[],
  N extends number,
  Current extends any[] = [],
  Res extends any[] = []
> = Arr extends [infer First, ...infer Rest]
  ? Current["length"] extends N
    ? Chunk<Rest, N, [First], [...Res, Current]>
    : Chunk<Rest, N, [...Current, First], Res>
  : [...Res, Current];

type res = Chunk<arr, 2>;

TupleToNestedObject

我们希望实现这样一个功能:

根据数组类型,比如 [‘a’, ‘b’, ‘c’] 的元组类型,再加上值的类型 'xxx',构造出这样的索引类型:

{
    a: {
        b: {
            c: 'xxx'
        }
    }
}

这个依然是提取、构造、递归,只不过是对数组类型做提取,构造索引类型,然后递归的这样一层层处理。

type TupleToNestedObject<T extends unknown[], V> = T extends [
  infer First,
  ...infer Rest
]
  ? {
      [K in First & (string | number | symbol)]: Rest extends any[]
        ? TupleToNestedObject<Rest, V>
        : V;
    }
  : V;

type res = TupleToNestedObject<["a", "b", 2, 4, "beyond"], 6>;
// type res = {
//   a: {
//     b: {
//       2: {
//         4: {
//           beyond: 6;
//         };
//       };
//     };
//   };
// };

注意K in First无需keyof,因为First本身就是一个具体的类型了。其次是作为索引,需要满足是 string | number | symbol 类型

PartialObjectPropByKeys

把一个索引类型的某些 Key 转为 可选的,其余的 Key 不变

type Copy<Obj extends Record<string, any>> = {
  [Key in keyof Obj]: Obj[Key];
};

type PartialObjectPropByKeys<
  R extends Record<string, any>,
  U extends keyof P
> = Copy<Partial<Pick<P, U>> & Omit<R, U>>;

type P = {
  name: string;
  age: number;
  hobby: any[];
};

type res = PartialObjectPropByKeys<P, "hobby" | "age">;
// type res = {
//   hobby?: any[] | undefined;
//   age?: number | undefined;
//   name: string;
// }

这里的Copy是为了构造映射类型让ts做类型推断,否则推出来的值是未经计算的交叉类型,编译器里不能直接显示结果。但实际功能上,加和不加Copy都是一样的。

函数,参数与返回值

函数的四种重载方式

同时声明

declare function func(name: string): string;
declare function func(name: number): number;

函数实现

function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: any, b: any) {
    return a + b;
}

接口约束

interface Fun {
  (name: string): string;
  (name: number): number;
}
declare const fun: Fun;

类型交叉

type Fun = (name: string) => string & ((name: number) => number);
declare const fun: Fun;

注意

如果函数是重载函数,TS的内置类型ReturnType只会返回最后一次重载时返回的类型

UnionToTuple

'a' | 'b' | 'c' 转成 ['a', 'b', 'c'] 已知:

  1. 重载函数能通过函数交叉的方式声明
  2. 能实现联合转交叉 那么:就能拿到联合类型的最后一个类型 以下Type在体操基础有说明,利用函数参数逆变,来获取交叉类型
type UnionToIntersection<U> = 
    (U extends U ? (x: U) => unknown : never) extends (x: infer R) => unknown
        ? R
        : never

type UnionToFuncIntersection<T> = UnionToIntersection<T extends any ? () => T : never>;

UnionToFuncIntersection作用:

我们对联合类型 T 做下处理,用 T extends any 触发分布式条件类型的特性,它会把联合类型的每个类型单独传入做计算,最后把计算结果合并成联合类型。把每个类型构造成一个函数类型传入。

这样,返回的交叉类型也就达到了函数重载的目的:

type res = UnionToFuncIntersection<"beyond" | "typescript">;
// type res = (() => "beyond") & (() => "typescript")

那么就可以使用ReturnType取最后一个类型,再利用Exclude把最后一个类型从联合类型中去掉,参与递归,构成元组

type UnionToTuple<U> = UnionToFuncIntersection<U> extends () => infer RT
  ? [...UnionToTuple<Exclude<U, RT>>, RT]
  : [];

type res = UnionToTuple<"beyond" | "typescript" | "2023">;
// type res = ["beyond", "typescript", "2023"]

完整代码

type UnionToIntersection<U> = (
  U extends U ? (x: U) => unknown : never
) extends (x: infer R) => unknown
  ? R
  : never;

type UnionToFuncIntersection<T> = UnionToIntersection<
  T extends any ? () => T : never
>;

type UnionToTuple<U> = UnionToFuncIntersection<U> extends () => infer RT
  ? [...UnionToTuple<Exclude<U, RT>>, RT]
  : [];

type res = UnionToTuple<"beyond" | "typescript" | "2023">;
// type res = ["beyond", "typescript", "2023"]

currying

有这样一个 curring 函数,接受一个函数,返回柯里化后的函数。

// 传入
const func = (a: string, b: number, c: boolean) => {};
// 返回
(a: string) => (b: number) => (c: boolean) => void

declare function currying(fn: xxx): xxx;

明显,这里返回值类型和参数类型是有关系的,所以要用类型编程。

传入的是函数类型,可以用模式匹配提取参数和返回值的类型来,构造成新的函数类型返回。

每有一个参数就返回一层函数,具体层数是不确定的,所以要用递归。

type CurriedFunc<Params, Return> = Params extends [infer Arg, ...infer Rest]
  ? (arg: Arg) => CurriedFunc<Rest, Return>
  : never;

declare function currying<Func>(
  fn: Func
): Func extends (...args: infer Params) => infer Result
  ? CurriedFunc<Params, Result>
  : never;

const res = currying((a: string, b: boolean, c: Date) => {});
// const res: (arg: string) => (arg: boolean) => (arg: Date) => never

join

const res = join('-')('beyond', 'and', 'typescript');

有这样一个 join 函数,它是一个高阶函数,第一次调用传入分隔符,第二次传入多个字符串,然后返回它们 join 之后的结果。

定义函数

declare function join<Delimiter extends string>(
  delimiter: Delimiter
): <Items extends string[]>(...parts: Items) => JoinType<Items, Delimiter>;

实现JoinType

type JoinType<
  Items extends any[],
  Delimiter extends string,
  Res extends string = ""
> = Items extends [infer First, ...infer Rest]
  ? JoinType<
      Rest,
      Delimiter,
      Res extends ""
        ? `${First & string}`
        : `${Res}${Delimiter}${First & string}`
    >
  : Res;

type res = JoinType<["beyond", "typescript", "2023"], "-">;
// type res = "beyond-typescript-2023"

这样就实现啦,完整代码

type JoinType<
  Items extends any[],
  Delimiter extends string,
  Res extends string = ""
> = Items extends [infer First, ...infer Rest]
  ? JoinType<
      Rest,
      Delimiter,
      Res extends ""
        ? `${First & string}`
        : `${Res}${Delimiter}${First & string}`
    >
  : Res;

type res = JoinType<["beyond", "typescript", "2023"], "-">;
// type res = "beyond-typescript-2023"

declare function join<Delimiter extends string>(
  delimiter: Delimiter
): <Items extends string[]>(...parts: Items) => JoinType<Items, Delimiter>;

const res2 = join("&")("beyond", "study", "typescript");
// const res2: "beyond&study&typescript"

DeepCamelize

比如这样一个索引类型:

type obj = {
    aaa_bbb: string;
    bbb_ccc: [
        {
            ccc_ddd: string;
        },
        {
            ddd_eee: string;
            eee_fff: {
                fff_ggg: string;
            }
        }
    ]
}

要求转成这样:

type DeepCamelizeRes = {
    aaaBbb: string;
    bbbCcc: [{
        cccDdd: string;
    }, {
        dddEee: string;
        eeeFff: {
            fffGgg: string;
        };
    }];
}

直接看代码

type DeepCamelize<Obj extends Record<string, any>> = Obj extends any[]
  ? CamelizeArr<Obj>
  : {
      [K in keyof Obj as K extends `${infer First}_${infer Rest}`
        ? `${First}${Capitalize<Rest>}`
        : K]: DeepCamelize<Obj[K]>;
    };

type CamelizeArr<Arr extends any[]> = Arr extends [infer First, ...infer Rest]
  ? [
      DeepCamelize<First extends Record<string, any> ? First : never>,
      ...CamelizeArr<Rest>
    ]
  : [];

type res = DeepCamelize<{
  aaa_bbb: string;
  bbb_ccc: [
    {
      ccc_ddd: string;
    },
    {
      ddd_eee: string;
      eee_fff: {
        fff_ggg: string;
      };
    }
  ];
}>;
// type res = {
//   aaaBbb: string;
//   bbbCcc: [{
//       cccDdd: string;
//   }, {
//       dddEee: string;
//       eeeFff: {
//           fffGgg: string;
//       };
//   }];
// }

AllKeyPath

type Obj = {
    a: {
        b: {
            b1: string
            b2: string
        }
        c: {
            c1: string;
            c2: string;
        }
    },
}

希望返回 a、a.b、a.b.b1、a.b.b2、a.c、a.c.c1、a.c.c2 这些全部的 path。

type Obj = {
  a: {
    b: {
      b1: string;
      b2: string;
    };
    c: {
      c1: string;
      c2: string;
    };
  };
};
type AllKeyPath<Obj extends Record<string, any>> = {
  [Key in keyof Obj]: Key extends string
    ? Obj[Key] extends Record<string, any>
      ? Key | `${Key}.${AllKeyPath<Obj[Key]>}`
      : Key
    : never;
}[keyof Obj];

type res = AllKeyPath<Obj>;
// type res = "a" | "a.b" | "a.c" | "a.b.b1" | "a.b.b2" | "a.c.c1" | "a.c.c2"

参数 Obj 是待处理的索引类型,通过 Record<string, any> 约束。

用映射类型的语法,遍历Key,并在 value 部分根据每个 Key 去构造以它为开头的 path

因为推导出来的 Key 默认是 unknown,而其实明显是个 string,所以 Key extends string 判断一下,后面的分支里 Key 就都是 string 了。

如果 Obj[Key] 依然是个索引类型的话,就递归构造,否则,返回当前的 Key

我们最终需要的是 value 部分,所以取 [keyof Obj] 的值。keyof Objkey 的联合类型,那么传入之后得到的就是所有 key 对应的 value 的联合类型。

这样就完成了所有 path 的递归生成

Defaultize

实现这样一个高级类型,对 A、B 两个索引类型做合并,如果是只有 A 中有的不变,如果是 A、B 都有的就变为可选,只有 B 中有的也变为可选。

type A = {
  a: 1;
  b: 2;
  c: 3;
};
type B = {
  b: 4;
  c: 5;
  d: 6;
};
type Defaultize<A, B> = Pick<A, Exclude<keyof A, keyof B>> &
  Partial<Pick<A, Extract<keyof A, keyof B>>> &
  Partial<Pick<B, Exclude<keyof B, keyof A>>>;

type Copy<T> = {
  [K in keyof T]: T[K];
};

type res = Copy<Defaultize<A, B>>;
// type res = {
//   a: 1;
//   b?: 2 | undefined;
//   c?: 3 | undefined;
//   d?: 6 | undefined;
// }

infer extends语法

type Demo<T extends string[]> = T extends [infer F, ...infer R] ? `${F}` : T;
// 不能将类型“F”分配给类型“string | number | bigint | boolean | null | undefined”。

type Demo2<T extends string[]> = T extends [infer F, ...infer R]
  ? `${F extends string ? F : never}`
  : T;
type Demo3<T extends string[]> = T extends [infer F, ...infer R]
  ? `${F & string}`
  : T;
type Demo4<T extends string[]> = T extends [infer F extends string, ...infer R]
  ? `${F}`
  : T;

用于推导类型时的断言,会进行类型转换。