typescript 在未来的重要性不用多说,但泛型知识点十分难以理解,导致typescript的水平无法得到质的提高,为此最近准备了一些练习题,让大家一起突破ts的瓶颈。
题目一:CapitalizeString
要求:
实现 CapitalizeString 泛型 将字符串的第一个字符转成大些,如果不是字符串,则直接返回。
type a1 = CapitalizeString<"handler">; // Handler
type a2 = CapitalizeString<"parent">; // Parent
type a3 = CapitalizeString<233>; // 233
思路:
- 使用refer获取第一个字符,转成大写。
- 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
思路:
- 使用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
思路:
- 使用infer关键字。
- prev 临时变量。
- 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<"">; // []
思路:
- 使用infer关键字。
- 递归思想。
- 三点运算符展开数组
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'
思路:
- 使用infer关键字。
- 递归思想。
- 使用泛型,增加一个变量使用。
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>; // ''
思路:
- 使用infer关键字。
- 增加多个变量
- 用泛型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"]
思路:
- 使用infer关键字。
- 增加多个变量
- 用泛型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
思路:
- 使用infer关键字。
- 用泛型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
思路:
- 使用infer关键字。
- 使用Uppercase判断是否是大写字母。
- 如果是大写字母,加 ‘-’ 和 转成小写字母输出。
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
思路:
- 使用infer关键字。
- 使用Uppercase转成大写字母。
- 使用泛型F进行标记,RE表示最终输出结果,SPLIT表示分隔符。
- 遇到分隔符,进行标记,下一次喜欢转成大写,其他情况,正常赋值
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,没有到最后一个属性
思路:
- 使用infer关键字。
- 使用内置泛型Record<string, any> 表示是否为对象。
- 如果是对象继续递归便利,并且让 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>;
思路:
- 使用keyof关键字,进行遍历。
- 使用 as 进行别名替换。
- 使用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"
思路:
- keyof 进行便利。
- number 表示把数组展开。
- 使用递归进行深度展开。
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'] // 非空数据,正常使用
思路:
- 使用&的规则,都要满足条件。
// 方案一
type NonEmptyArray<T> = [T,...T[]]// 你的实现代码
// 方案二
type NonEmptyArray<T> = T[] & {0:T}// 你的实现代码
知识点总结:
- 使用infer。
- 使用递归思想。
- 使用泛型变量。
好好理解有不一样的感受。