火星文 -- typescript 泛型(一)

1,163 阅读3分钟

typescript 在未来的重要性不用多说,但泛型知识点十分难以理解,导致typescript的水平无法得到质的提高,为此最近准备了一些练习题,让大家一起突破ts的瓶颈。

题目一:CapitalizeString

要求:

实现 CapitalizeString 泛型 将字符串的第一个字符转成大些,如果不是字符串,则直接返回。

type a1 = CapitalizeString<"handler">; // Handler
type a2 = CapitalizeString<"parent">; // Parent
type a3 = CapitalizeString<233>; // 233

思路:

  1. 使用refer获取第一个字符,转成大写。
  2. Uppercase 是 lib.es5.d.ts 的内置方法。

答案

type CapitalizeString<T> = T extends `${infer L}${infer R}`
  ? `${Uppercase<L>}${R}` : T;

题目二:FirstChar

要求:

实现 FirstChar 泛型 获取字符串的第一个字母,如果字符串为空,则返回never;

type b1 = FirstChar<"BFE">; // 'B'
type b2 = FirstChar<"dev">; // 'd'
type b3 = FirstChar<"">; // never

思路:

  1. 使用refer获取第一个字符。
type FirstChar<Str extends string> = Str extends `${infer First}${infer Second}`
  ? `${First}`: never;

总结: 第一题和第二题的思路类似,都是使用 extends, 模版字符串, infer 关键字来解决问题。 ​

题目三:LastChar

要求:

实现 LastChar 泛型 获取字符串的最后一个字母,如果字符串为空,则返回never;

type c1 = LastChar<"BFE">; // 'E'
type c2 = LastChar<"dev">; // 'v'
type c3 = LastChar<"">; // never

思路:

  1. 使用infer关键字。
  2. prev 临时变量。
  3. extends 递归寻找。
type LastChar<
  Str extends string, //目标字符串
  perv = never // 输入字符串
  > = Str extends `${infer First}${infer Second}`
  ? LastChar<Second, First>
  : perv;

总结: 这次用到了递归思想。理解递归,理解上面的思路不难。

题目四:StringToTuple

要求:

实现 StringToTuple 泛型 将字符串转成元祖。

type d1 = StringToTuple<"BFE.dev">; // ['B', 'F', 'E', '.', 'd', 'e','v']
type d2 = StringToTuple<"">; // []

思路:

  1. 使用infer关键字。
  2. 递归思想。
  3. 三点运算符展开数组
type StringToTuple<T extends string> = T extends `${infer L}${infer R}`
  ? [L, ...StringToTuple<R>]
  : [];

题目四:TupleToString

要求:

实现 TupleToString 泛型 将元组转换成字符串。

type e1 = TupleToString<["a", "b", "c"]>; // 'abc'
type e2 = TupleToString<[]>; // ''
type e3 = TupleToString<["a"]>; // 'a'

思路:

  1. 使用infer关键字。
  2. 递归思想。
  3. 使用泛型,增加一个变量使用。
type TupleToString<T, prev extends string = ""> = T extends [
  infer L,
  ...infer R
]
  ? L extends string
  ? TupleToString<R, `${prev}${L}`>
  : never
  : prev;

题目五:RepeatString

要求:

实现 RepeatString 泛型 将一个泛型复制多分,变成字符串返回

type f1 = RepeatString<"a", 3>; // 'aaa'
type f2 = RepeatString<"a", 0>; // ''

思路:

  1. 使用infer关键字。
  2. 增加多个变量
  3. 用泛型A约束结束条件,S,存储临时结果。
type RepeatString<
  T extends string, // 字符串
  C extends number, // 遍历个数
  S extends string = "", // 临时结果
  A extends any[] = [] // 结束条件
  > = A["length"] extends C ? S : RepeatString<T, C, `${T}${S}`, [1, ...A]>;

总结: 都使用了扩展泛型的能力,增加了变量的使用。 ​

题目六:SplitString

要求:

实现 SplitString 泛型 将字符串按照一定的分隔符进行分割,并转成数组。

type g1 = SplitString<"handle-open-flag", "-">; // ["handle", "open", "flag"]
type g2 = SplitString<"open-flag", "-">; // ["open", "flag"]
type g3 = SplitString<"handle.open.flag", ".">; // ["handle", "open", "flag"]
type g4 = SplitString<"open.flag", ".">; // ["open", "flag"]
type g5 = SplitString<"open.flag", ".">; // ["open.flag"]

思路:

  1. 使用infer关键字。
  2. 增加多个变量
  3. 用泛型A存储最终结果,B存储临时结果。
type SplitString<
  T extends string, // 初始字符串
  C extends string, // 分隔符
  B extends string = "", //临时结果
  A extends any[] = [] // 最终结果  SplitString<T,C,S,B,A>
  > = T extends `${infer L}${infer R}`
  ? L extends C
  ? SplitString<R, C, ``, [...A, B]>
  : SplitString<R, C, `${B}${L}`, A>
  : [...A, B];

题目七:LengthOfString

要求:

实现 LengthOfString 泛型 计算一个字符串的个数

type h1 = LengthOfString<"BFE.dev">; // 7
type h2 = LengthOfString<"">; // 0

思路:

  1. 使用infer关键字。
  2. 用泛型RE存储最终结果,使用length获取长度返回。
type LengthOfString<T, RE extends any[] = []> = T extends `${infer L}${infer R}`
  ? LengthOfString<R, [L, ...RE]>
  : RE["length"];

题目八:KebabCase

要求:

实现 KebabCase 泛型,将驼峰转成字符串输出。

type i1 = KebabCase<"HandleOpenFlag">; // handle-open-flag
type i2 = KebabCase<"OpenFlag">; // open-flag

思路:

  1. 使用infer关键字。
  2. 使用Uppercase判断是否是大写字母。
  3. 如果是大写字母,加 ‘-’ 和 转成小写字母输出。
type KebabCase<T, RE extends string = ""> = T extends `${infer L}${infer R}`
  ? Uppercase<L> extends L
  ? RE extends ""
  ? KebabCase<R, `${Lowercase<L>}`>
  : KebabCase<R, `${RE}-${Lowercase<L>}`>
  : KebabCase<R, `${RE}${Lowercase<L>}`>
  : RE;

题目九:CamelCase

要求:

实现 KebabCase 泛型,将驼峰转成字符串输出。

type j1 = CamelCase<"handle-open-flag">; // HandleOpenFlag
type j2 = CamelCase<"open-flag">; // OpenFlag

思路:

  1. 使用infer关键字。
  2. 使用Uppercase转成大写字母。
  3. 使用泛型F进行标记,RE表示最终输出结果,SPLIT表示分隔符。
  4. 遇到分隔符,进行标记,下一次喜欢转成大写,其他情况,正常赋值
type CamelCase<
  T, // 输入字符串
  SPLIT extends string = "-",
  RE extends string = "", // 输出字符串
  F extends boolean = true // 标记
  > = T extends `${infer L}${infer R}`
  ? F extends true
    ? CamelCase<R, SPLIT, `${RE}${Uppercase<L>}`, false>
    : L extends SPLIT 
      ? CamelCase<R, SPLIT, `${RE}`, true>
      : CamelCase<R, SPLIT, `${RE}${L}`, false>
  : RE;

总结: 由于判断分支比较多,且只能用三目运算符,所以采用缩进的方法,方便阅读。 ​

题目十一:CamelCase

要求:

实现 ObjectAccessPaths 泛型,将对象转成可以输出的泛型

// 简单来说,就是根据如下对象类型:
/*
{
    home: {
        topBar: {
            title: '顶部标题',
            welcome: '欢迎登录'
        },
        bottomBar: {
            notes: 'XXX备案,归XXX所有',
        },
    },
    login: {
        username: '用户名',
        password: '密码'
    }
}
*/
// 得到联合类型:
/*
home.topBar.title | home.topBar.welcome | home.bottomBar.notes | login.username | login.password
*/

// 完成 createI18n 函数中的 ObjectAccessPaths<Schema>,限制函数i18n的参数为合法的属性访问字符串
function createI18n<Schema>(
  schema: Schema
): (path: ObjectAccessPaths<Schema>) => string {
  return [{ schema }] as any;
}

// i18n函数的参数类型为:home.topBar.title | home.topBar.welcome | home.bottomBar.notes | login.username | login.password
const i18n = createI18n({
  home: {
    topBar: {
      title: "顶部标题",
      welcome: "欢迎登录",
    },
    bottomBar: {
      notes: "XXX备案,归XXX所有",
    },
  },
  login: {
    username: "用户名",
    password: "密码",
  },
});

i18n("home.topBar.title"); // correct
i18n("home.topBar.welcome"); // correct
i18n("home.bottomBar.notes"); // correct

// i18n('home.login.abc')              // error,不存在的属性
// i18n('home.topBar')                 // error,没有到最后一个属性

思路:

  1. 使用infer关键字。
  2. 使用内置泛型Record<string, any> 表示是否为对象。
  3. 如果是对象继续递归便利,并且让 Prex泛型接受临时变量
type ObjectAccessPaths<
  T,
  Prev extends string = "",
  K = keyof T
  > = K extends keyof T
  ? K extends string
    ? T[K] extends Record<string, any>
      ? ObjectAccessPaths<T[K], `${Prev}${K}.`>
      : `${Prev}${K}`
    : never
  : never;

题目十二:ComponentEmitsType

要求:

实现 ComponentEmitsType 泛型,将函数转成事件类型。

// 实现 ComponentEmitsType<Emits> 类型,将
/*
{
    'handle-open': (flag: boolean) => true,
    'preview-item': (data: { item: any, index: number }) => true,
    'close-item': (data: { item: any, index: number }) => true,
}
*/
// 转化为类型
/*
{
    onHandleOpen?: (flag: boolean) => void,
    onPreviewItem?: (data: { item: any, index: number }) => void,
    onCloseItem?: (data: { item: any, index: number }) => void,
}
*/
const Source = {
  "handle-open": (flag: boolean) => true,
  "preview-item": (data: { item: any; index: number }) => true,
  "close-item": (data: { item: any; index: number }) => true,
};
type SourceEmit = ComponentEmitsType<typeof Source>;

思路:

  1. 使用keyof关键字,进行遍历。
  2. 使用 as 进行别名替换。
  3. 使用infer 提取函数参数类型。
// 最后返回的 Component变量类型为一个合法的React组件类型,并且能够通过`on事件驼峰命名`的方式,监听定义的事件,并且能够自动推导出事件的参数类型

type ComponentEmitsType<T> = {
  [P in keyof T as `on${CamelCase<P>}`]: T[P] extends (...args: infer A) => any
  ? (...args: A) => void
  : T[P];
};

扩展:

// 提示,定义组件的props类型方式为 { (props: Partial<Convert<Emits>>): any }
// 比如 Comp 可以接收属性 {name:string, age:number, flag:boolean, id?:string},其中id为可选属性,那么可以这样写

const Comp: {
  (props: { name: string; age: number; flag: boolean; id?: string }): any;
} = Function as any;

console.log(<Comp name="" age={1} flag />); // 正确
console.log(<Comp name="" age={1} flag id="111" />); // 正确
// console.log(<Comp name={1} age={1} flag/>)          // 错误,name为字符串类型
// console.log(<Comp age={1} flag/>)                   // 错误,缺少必须属性name:string



const Source = {
  "handle-open": (flag: boolean) => true,
  "preview-item": (data: { item: any; index: number }) => true,
  "close-item": (data: { item: any; index: number }) => true,
};
type SourceEmit = ComponentEmitsType<typeof Source>;
const Component: { (props: SourceEmit): any } = (emits) => {
  return [{ emits }] as any;
};
console.log(
  <Component
    // onHandleOpen 的类型为 (flag: boolean) => void
    onHandleOpen={(val: any) => console.log(val.valueOf())}
    // onPreviewItem 的类型为 (data: { item: any, index: number }) => void
    onPreviewItem={(val: any) => {
      const { item, index } = val;
      const a: number = item;
      console.log(a, index.toFixed(2));
    }}
    // 所有的监听事件属性都是可选属性,可以不传处理函数句柄
    onCloseItem={(val) => [{ val }]}
  />
);

这个是组件的属性和事件泛型的实现方式。

题目十三:NaiveFlats

要求:

实现 NaiveFlats 泛型,将数组字符串拍平。


  type NaiveResult = NaiveFlats<[['a','s',['g']], ['b', 'c'], ['d']]>
// NaiveResult的结果: "a" | "d" | "c" | "s" | "g" | "b"

思路:

  1. keyof 进行便利。
  2. number 表示把数组展开。
  3. 使用递归进行深度展开。
type NaiveFlat<T extends any[]> = {
  [P in keyof T]: T[P] extends any[] ? T[P][number] : T[P]
}[number]

type NaiveFlats<T extends any[]>  = {
  [P in keyof T]: T[P] extends any[] ? NaiveFlat<T[P]> : T[P]
}[number]

type NaiveResult1 = NaiveFlat<['a','s','d']>

总结:很多人看到number会感觉到困惑,你可以把number当作 keyof 特殊关键字来理解即可。

题目十四:NonEmptyArray

要求:

实现 NonEmptyArray 泛型,提示数组不能为空。

const a: NonEmptyArray <string> =[] // 将出现编译错误
const b: NonEmptyArray<string> = ['Hello TS','www','22'] // 非空数据,正常使用

思路:

  1. 使用&的规则,都要满足条件。
// 方案一
type NonEmptyArray<T> = [T,...T[]]// 你的实现代码

// 方案二
type NonEmptyArray<T> = T[] & {0:T}// 你的实现代码

知识点总结:

  1. 使用infer。
  2. 使用递归思想。
  3. 使用泛型变量。

好好理解有不一样的感受。