返回

剖析TypeScript挑战赛的中级难题:迎接编程成长的考验

前端

披荆斩棘:迎战TypeScript挑战赛

在掌握TypeScript的基础后,勇于挑战更高难度的问题是夯实功底的不二之选。TypeScript挑战赛精心挑选一系列难题,旨在考验学习者的编程能力与对类型系统的理解,助其成长为更优秀的程序员。本系列文章将带领您踏上通关之旅,逐一破解这些精心设计的难题。

挑战题57:裁剪右侧,去空白,截断它!

挑战内容:

编写一个TrimRight类型,将字符串类型T的右侧空白裁剪掉。

type TrimRight<' ' | ' Hello World ' | ' TypeScript   '> = '' | 'Hello World' | 'TypeScript'

解决方案:

type TrimRight<T extends string> =
    T extends `${infer First}${infer Rest}`
        ? First extends ' ' | '\n' | '\t'
            ? TrimRight<Rest>
            : T
        : T;

简析:

此解决方案利用递归将字符串的右侧空白逐一裁剪掉。首先,它将字符串类型T分解成首字母First和剩余部分Rest。如果First是空白字符(空格、换行或制表符),则继续递归调用TrimRight来裁剪剩余部分。否则,直接返回T,表明字符串中不再有右侧空白。通过这种方式,字符串的右侧空白被逐一去除,最终返回裁剪后的字符串。

挑战题58:在没有类型提示的情况下提取数组元素

挑战内容:

编写一个ElementAt<T, N>类型,用于从T数组中提取第N个元素。

type ElementAt<[1, 2, 3], 0> = 1
type ElementAt<['a', 'b', 'c'], 1> = 'b'

解决方案:

type ElementAt<T extends unknown[], N extends number> =
    N extends T['length']
        ? never
        : T extends [infer First, ...infer Rest]
            ? N extends 0
                ? First
                : ElementAt<Rest, N>
            : never;

简析:

此解决方案巧妙地运用了类型卫语句,在运行时进行类型检查。首先,它判断N是否等于数组T的长度,如果是,则返回never,表示数组中不存在第N个元素。然后,它利用模式匹配将数组T分解成首元素First和剩余部分Rest。如果N为0,则返回首元素First,即数组的第一个元素。否则,继续递归调用ElementAt<Rest, N>来提取剩余部分的第N个元素。通过这种方式,可以从数组中提取指定位置的元素。

挑战题59:生成数字序列

挑战内容:

编写一个Range<From, To>类型,用于生成从From到To(包括To)的数字序列。

type Range<From extends number, To extends number> = From extends To
    ? [From]
    : [...Range<From, To - 1>, To];

解决方案:

type Range<From extends number, To extends number> =
    From extends To
        ? [From]
        : [...Range<From, To - 1>, To];

简析:

此解决方案使用了递归的方式生成数字序列。首先,它判断From是否等于To,如果是,则直接返回一个包含From的数组,表示序列只包含一个元素。然后,它利用展开运算符将Range<From, To - 1>的序列与To连接起来,从而生成从From到To的数字序列。通过这种方式,可以生成指定范围内的数字序列。

挑战题60:从数组中生成对象

挑战内容:

编写一个FromEntries类型,将T数组中的键值对转换为对象。

type FromEntries<[
    ['name', 'Gene'],
    ['age', 20],
    ['city', 'Shenzhen']
]> = {
    name: 'Gene',
    age: 20,
    city: 'Shenzhen',
};

解决方案:

type FromEntries<T extends [string, unknown][]> = {
    [K in T[number][0]]: T[number][1];
};

简析:

此解决方案使用了映射类型来将数组中的键值对转换为对象。首先,它定义了一个类型参数T,表示一个字符串和未知类型的数组。然后,它利用映射类型将T[number][0](键)映射到T[number][1](值),从而生成一个对象类型。最后,利用索引签名将对象类型转换为实际的对象,完成键值对的转换。

挑战题61:键重命名

挑战内容:

编写一个RenameKey<T, K, NewK>类型,将T对象中的K键重命名为NewK。

type RenameKey<T, K extends keyof T, NewK extends string> = {
    [P in keyof T]: P extends K ? NewK : P;
} & { [NewK]: T[K]; };

type A = {
    name: string;
    age: number;
};

type B = RenameKey<A, 'age', 'years'>;
// { name: string, years: number }

解决方案:

type RenameKey<T, K extends keyof T, NewK extends string> = {
    [P in keyof T]: P extends K ? NewK : P;
} & { [NewK]: T[K]; };

简析:

此解决方案巧妙地利用了映射类型和交叉类型来实现键的重命名。首先,它定义了三个类型参数:T(对象类型)、K(要重命名的键)、NewK(新键名)。然后,它利用映射类型将T的键P映射到P extends K ? NewK : P,这意味着如果P是K,则将其映射到NewK,否则保持不变。最后,它使用交叉类型将映射类型与一个对象类型合并,该对象类型包含一个键NewK,其值为T[K],从而完成键的重命名。

挑战题62:对象的反转

挑战内容:

编写一个ReverseObj类型,将T对象的反转,使其键成为值,值成为键。

type ReverseObj<T> = {
    [P in keyof T]: T[P] extends object ? ReverseObj<T[P]> : T[P];
};

type A = {
    name: string;
    age: number;
    friends: {
        bestFriend: string;
    };
};

type B = ReverseObj<A>;
// {
//     Gene: string;
//     20: number;
//     bestFriend: {
//         Gene: string;
//     }
// }

解决方案:

type ReverseObj<T> = {
    [P in keyof T]: T[P] extends object ? ReverseObj<T[P]> : T[P];
};

简析:

此解决方案使用了递归和映射类型来实现对象的逆转。首先,它定义了一个类型参数T,表示要逆转的对象类型。然后,它利用映射类型将T的键P映射到T[P]。如果T[P]是对象类型,则继续递归调用ReverseObj<T[P]>来逆转它,否则直接返回T[P]。通过这种方式,可以将对象的键和值互换,从而实现对象的逆转。

挑战与机遇并存

TypeScript挑战赛是一项极具挑战性的活动,它旨在帮助学习者掌握TypeScript的精髓,并在编程的道路上更进一步。通过解决这些精心设计的难题,学习者可以加深对类型系统的理解,掌握更高级的编程技巧,并培养解决复杂问题的能力。虽然挑战重重,但机遇亦伴随左右,勇于迎接挑战,不断学习,方能成为一名优秀的程序员。