小镇做题家 - TypeScript 类型大挑战(中等篇 - 中)

前端开发
2022年09月25日
0

Medium 组(中)

KebabCase

Replace the camelCase or PascalCase string with kebab-case.

typescript
type KebabCase<S> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<KebabCase<'FooBarBaz'>, 'foo-bar-baz'>>, Expect<Equal<KebabCase<'fooBarBaz'>, 'foo-bar-baz'>>, Expect<Equal<KebabCase<'foo-bar'>, 'foo-bar'>>, Expect<Equal<KebabCase<'foo_bar'>, 'foo_bar'>>, Expect<Equal<KebabCase<'Foo-Bar'>, 'foo--bar'>>, Expect<Equal<KebabCase<'ABC'>, 'a-b-c'>>, Expect<Equal<KebabCase<'-'>, '-'>>, Expect<Equal<KebabCase<''>, ''>> ]

在 TypeScript 的工具类中,有一个工具是可以把首字母转成小写,它就是 Uncapitalize

typescript
type A = 'HELLO WORLD' type E = Expect<Equal<Uncapitalize<A>, 'hELLO WORLD'>>

因为这一题里面首字母是大写时,只需要把它转成小写即可,而不需要再加上 -,所以我们需要对剩余字符进行判断:

typescript
type KebabCase<S> = S extends `${infer F}${infer R}` ? R extends Uncapitalize<R> ? `${Lowercase<F>}${KebabCase<R>}` : `${Lowercase<F>}-${KebabCase<R>}` : S

Diff

Get an Object that is the difference between O & O1

typescript
type Diff<O, O1> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type Foo = { name: string age: string } type Bar = { name: string age: string gender: number } type Coo = { name: string gender: number } type cases = [ Expect<Equal<Diff<Foo, Bar>, { gender: number }>>, Expect<Equal<Diff<Bar, Foo>, { gender: number }>>, Expect<Equal<Diff<Foo, Coo>, { age: string; gender: number }>>, Expect<Equal<Diff<Coo, Foo>, { age: string; gender: number }>>, ]

取差集,基操,只需要去除交集即可:

typescript
type Diff<O, O1> = { [P in Exclude<keyof O, keyof O1> | Exclude<keyof O1, keyof O>]: P extends keyof O ? O[P] : P extends keyof O1 ? O1[P] : never }

AnyOf

Implement Python liked any function in the type system. A type takes the Array and returns true if any element of the Array is true. If the Array is empty, return false.

typescript
type AnyOf<T extends readonly any[]> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<AnyOf<[1, 'test', true, [1], { name: 'test' }, { 1: 'test' }]>, true>>, Expect<Equal<AnyOf<[1, '', false, [], {}]>, true>>, Expect<Equal<AnyOf<[0, 'test', false, [], {}]>, true>>, Expect<Equal<AnyOf<[0, '', true, [], {}]>, true>>, Expect<Equal<AnyOf<[0, '', false, [1], {}]>, true>>, Expect<Equal<AnyOf<[0, '', false, [], { name: 'test' }]>, true>>, Expect<Equal<AnyOf<[0, '', false, [], { 1: 'test' }]>, true>>, Expect<Equal<AnyOf<[0, '', false, [], { name: 'test' }, { 1: 'test' }]>, true>>, Expect<Equal<AnyOf<[0, '', false, [], {}]>, false>>, Expect<Equal<AnyOf<[]>, false>>, ]

只要传入的数组中有一项为 true,则结果为 true。首先我们需要知道哪些值是 falsy 值,从 cases 中可以得知:

typescript
type Falsy = 0 | '' | [] | false | Record<PropertyKey, never>

需要注意的是,空对象类型得采用 Record<PropertyKey, never> 来表示。如此一来,解题也变得非常简单了:

typescript
type Falsy = 0 | '' | [] | false | Record<PropertyKey, never> type AnyOf<T extends readonly any[]> = T[number] extends Falsy ? false : true

IsNever

Implement a type IsNever, which takes input type T.

If the type of resolves to never, return true, otherwise false.

typescript
type IsNever<T> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<IsNever<never>, true>>, Expect<Equal<IsNever<never | string>, false>>, Expect<Equal<IsNever<''>, false>>, Expect<Equal<IsNever<undefined>, false>>, Expect<Equal<IsNever<null>, false>>, Expect<Equal<IsNever<[]>, false>>, Expect<Equal<IsNever<{}>, false>>, ]

我总感觉这题不应该出现在这个栏目:

typescript
type IsNever<T> = [T] extends [never] ? true : false

IsUnion

Implement a type IsUnion, which takes an input type T and returns whether T resolves to a union type.

typescript
type IsUnion<T> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<IsUnion<string>, false>>, Expect<Equal<IsUnion<string | number>, true>>, Expect<Equal<IsUnion<'a' | 'b' | 'c' | 'd'>, true>>, Expect<Equal<IsUnion<undefined | null | void | ''>, true>>, Expect<Equal<IsUnion<{ a: string } | { a: number }>, true>>, Expect<Equal<IsUnion<{ a: string | number }>, false>>, Expect<Equal<IsUnion<[string | number]>, false>>, // Cases where T resolves to a non-union type. Expect<Equal<IsUnion<string | never>, false>>, Expect<Equal<IsUnion<string | unknown>, false>>, Expect<Equal<IsUnion<string | any>, false>>, Expect<Equal<IsUnion<string | 'a'>, false>>, Expect<Equal<IsUnion<never>, false>>, ]

首先,我们先要把 never 排除掉:

typescript
type IsUnion<T, A = T> = [T] extends [never] ? false : // ...

如果传入的是联合类型,那么就拿第一项和整个联合类型进行对比:

typescript
type IsUnion<T, A = T> = [T] extends [never] ? false : T extends A ? // ... : // ...

如果 T 是一个非联合类型,那么 T extends A 这条语句永远都会是 true,所以我们还需要进一步判断,通过一个小技巧:

typescript
type IsUnion<T, A = T> = [T] extends [never] ? false : T extends A ? Equal<[T], [A]> extends true ? false : true : false

如果 T 是一个联合类型,假设是:string | number,那么 [T] 就是 [string],而 [A] 就是 [string | number] ,它们两者必不相等;如果 T 是一个非联合类型,假设是:string,那么 [T][A] 都是 [string],两者相等。所以以此来判断它是否为一个联合类型。

ReplaceKeys

Implement a type ReplaceKeys, that replace keys in union types, if some type has not this key, just skip replacing,

A type takes three arguments.

typescript
type ReplaceKeys<U, T, Y> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type NodeA = { type: 'A' name: string flag: number } type NodeB = { type: 'B' id: number flag: number } type NodeC = { type: 'C' name: string flag: number } type ReplacedNodeA = { type: 'A' name: number flag: string } type ReplacedNodeB = { type: 'B' id: number flag: string } type ReplacedNodeC = { type: 'C' name: number flag: string } type NoNameNodeA = { type: 'A' flag: number name: never } type NoNameNodeC = { type: 'C' flag: number name: never } type Nodes = NodeA | NodeB | NodeC type ReplacedNodes = ReplacedNodeA | ReplacedNodeB | ReplacedNodeC type NodesNoName = NoNameNodeA | NoNameNodeC | NodeB type cases = [ Expect<Equal<ReplaceKeys<Nodes, 'name' | 'flag', { name: number; flag: string }>, ReplacedNodes>>, Expect<Equal<ReplaceKeys<Nodes, 'name', { aa: number }>, NodesNoName>>, ]

题目中指出需要三个泛型参数,分别是:源类型(U)、需要的键(T)以及替换键组成的接口(Y),而我们要做的是,把 U 中所有符合 T 的键都替换成 Y 里面的类型。

所以解题的方法也很简单了,把需要匹配的条件都列出来即可:

typescript
type ReplaceKeys<U, T, Y> = { [P in keyof U]: P extends T ? P extends keyof Y ? Y[P] : never : U[P] }

Remove Index Signature

Implement RemoveIndexSignature<T> , exclude the index signature from object types.

typescript
type RemoveIndexSignature<T> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type Foo = { [key: string]: any foo(): void } type Bar = { [key: number]: any bar(): void 0: string } const foobar = Symbol('foobar') type FooBar = { [key: symbol]: any [foobar](): void } type Baz = { bar(): void baz: string } type cases = [ Expect<Equal<RemoveIndexSignature<Foo>, { foo(): void }>>, Expect<Equal<RemoveIndexSignature<Bar>, { bar(): void; 0: string }>>, Expect<Equal<RemoveIndexSignature<FooBar>, { [foobar](): void }>>, Expect<Equal<RemoveIndexSignature<Baz>, { bar(): void; baz: string }>>, ]

从对象中移除索引类型,那么我们先需要判断哪些才是索引类型:

typescript
type IsSignature<T> = string extends T ? true : number extends T ? true : symbol extends T ? true : false

从对象中删除一个键,将该键置为 never 即可:

typescript
type Obj = { a: string; b: number; } type DeleteKeys<T> = { [K in keyof T as never]: T[K] } type EmptyObj = DeleteKeys<Obj> // {}

所以最终答案如下代码所示:

typescript
type IsSignature<T> = string extends T ? true : number extends T ? true : symbol extends T ? true : false type RemoveIndexSignature<T> = { [P in keyof T as IsSignature<P> extends true ? never : P]: T[P] }

Percentage Parser

Implement PercentageParser.

According to the /^(\+|\-)?(\d*)?(\%)?$/ regularity to match T and get three matches.

typescript
type PercentageParser<A extends string> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type Case0 = ['', '', ''] type Case1 = ['+', '', ''] type Case2 = ['+', '1', ''] type Case3 = ['+', '100', ''] type Case4 = ['+', '100', '%'] type Case5 = ['', '100', '%'] type Case6 = ['-', '100', '%'] type Case7 = ['-', '100', ''] type Case8 = ['-', '1', ''] type Case9 = ['', '', '%'] type Case10 = ['', '1', ''] type Case11 = ['', '100', ''] type cases = [ Expect<Equal<PercentageParser<''>, Case0>>, Expect<Equal<PercentageParser<'+'>, Case1>>, Expect<Equal<PercentageParser<'+1'>, Case2>>, Expect<Equal<PercentageParser<'+100'>, Case3>>, Expect<Equal<PercentageParser<'+100%'>, Case4>>, Expect<Equal<PercentageParser<'100%'>, Case5>>, Expect<Equal<PercentageParser<'-100%'>, Case6>>, Expect<Equal<PercentageParser<'-100'>, Case7>>, Expect<Equal<PercentageParser<'-1'>, Case8>>, Expect<Equal<PercentageParser<'%'>, Case9>>, Expect<Equal<PercentageParser<'1'>, Case10>>, Expect<Equal<PercentageParser<'100'>, Case11>>, ]

这题可以分两步来判断,一个是以 '+' | '-' 开头,另一个是以 '%' 结束:

typescript
type PercentageParser<A extends string> = A extends `${infer S extends '+' | '-'}${infer R}` ? R extends `${infer F}%` ? [S, F, '%'] : [S, R, ''] : A extends `${infer F}%` ? ['', F, '%'] : ['', A, '']

Drop Char

Drop a specified char from a string.

typescript
type DropChar<S, C> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type cases = [ // @ts-expect-error Expect<Equal<DropChar<'butter fly!', ''>, 'butterfly!'>>, Expect<Equal<DropChar<'butter fly!', ' '>, 'butterfly!'>>, Expect<Equal<DropChar<'butter fly!', '!'>, 'butter fly'>>, Expect<Equal<DropChar<' butter fly! ', ' '>, 'butterfly!'>>, Expect<Equal<DropChar<' b u t t e r f l y ! ', ' '>, 'butterfly!'>>, Expect<Equal<DropChar<' b u t t e r f l y ! ', 'b'>, ' u t t e r f l y ! '>>, Expect<Equal<DropChar<' b u t t e r f l y ! ', 't'>, ' b u e r f l y ! '>>, ]

又是一道字符串操作题,递归处理即可:

typescript
type DropChar<S, C, Result extends string = ''> = S extends `${infer F}${infer R}` ? F extends C ? DropChar<R, C, Result> : DropChar<R, C, `${Result}${F}`> : Result

或者:

typescript
type DropChar<S extends string, C extends string> = S extends `${infer F}${infer R}` ? `${F extends C ? '' : F}${DropChar<R, C>}` : S

这题的 // @ts-expect-error 有点谜,就不处理了。

MinusOne

Given a number (always positive) as a type. Your type should return the number decreased by one.

typescript
type MinusOne<T extends number> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<MinusOne<1>, 0>>, Expect<Equal<MinusOne<55>, 54>>, Expect<Equal<MinusOne<3>, 2>>, Expect<Equal<MinusOne<100>, 99>>, Expect<Equal<MinusOne<1101>, 1100>>, ]

数组它承受了它不该承受的东西,只因为它有个 length,所以在做这种加减法的类型体操,我们只需要给个数组,最终拿到它的 Length 即可:

typescript
type Fill<N extends number, R extends number[] = []> = R['length'] extends N ? R : Fill<N, [...R, 0]> type MinusOne<T extends number, A extends number[] = []> = Fill<T> extends [infer F, ...infer R] ? R['length'] : never

但这一题,需要注意一下递归溢出问题,所以我们需要用其他办法来创建一个符合题目要求的数组:

typescript
type Dict = { '0': []; '1': [0]; '2': [0, 0]; '3': [0, 0, 0]; '4': [0, 0, 0, 0]; '5': [0, 0, 0, 0, 0]; '6': [0, 0, 0, 0, 0, 0]; '7': [0, 0, 0, 0, 0, 0, 0]; '8': [0, 0, 0, 0, 0, 0, 0, 0]; '9': [0, 0, 0, 0, 0, 0, 0, 0, 0]; } type FillTenTimes<A extends 0[] = []> = [ ...A, ...A, ...A, ...A, ...A, ...A, ...A, ...A, ...A, ...A ] type Fill<N extends string, Result extends 0[] = []> = N extends `${infer F extends keyof Dict}${infer R}` ? Fill<R, [...FillTenTimes<Result>, ...Dict[F]]> : Result type MinusOne<T extends number> = Fill<`${T}`> extends [infer F, ...infer R] ? R['length'] : T

这里解释一下这个数组是怎么被创建出来的,以最后一个 case 的 1101 为例:

  1. 交给 Fill 来填充的是字符串 1101,此时 F === '1'R === '101',初始的 Result[],它经过 FillTenTimes 处理后还是 []Dict[F][0],所以交给第二次递归的是 Fill<'101', [0]>
  2. 第二次递归时:F === '1'R === '01'Result === [0],然后 Result 经过 FillTenTimes 处理后变成 [10 * 0]Dict[F][0],所以交给第三次递归的是 Fill<'01', [10 * 0, 0]>
  3. 第三次递归时:F === '0'R === '1'Result === [11 * 0], 然后 Result 经过 FillTenTimes 处理后变成 [110 * 0]Dict[F][],所以交给第四次递归的是 Fill<'1', [110 * 0]>
  4. 第四次递归时:F === '1'R === ''Result === [110 * 0],然后 Result 经过 FillTenTimes 处理后变成 [1100 * 0]Dict[f][0],所以交给第五次递归的是 Fill<'', [1100 * 0, 0]>
  5. 第五次递归时,由于传入的是一个空字符串,所以条件不满足,此时返回 Result,也就是 [1101 * 1]

PickByType

From T, pick a set of properties whose type are assignable to U.

typescript
type PickByType<T, U> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' interface Model { name: string count: number isReadonly: boolean isEnable: boolean } type cases = [ Expect<Equal<PickByType<Model, boolean>, { isReadonly: boolean; isEnable: boolean }>>, Expect<Equal<PickByType<Model, string>, { name: string }>>, Expect<Equal<PickByType<Model, number>, { count: number }>>, ]

和 Pick 很类似,只不过是需要判断的是值:

typescript
type PickByType<T, U> = { [P in keyof T as T[P] extends U ? P : never]: T[P] }

StartsWith

Implement StartsWith<T, U> which takes two exact string types and returns whether T starts with U

typescript
type StartsWith<T extends string, U extends string> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<StartsWith<'abc', 'ac'>, false>>, Expect<Equal<StartsWith<'abc', 'ab'>, true>>, Expect<Equal<StartsWith<'abc', 'abcd'>, false>>, Expect<Equal<StartsWith<'abc', ''>, true>>, Expect<Equal<StartsWith<'abc', ' '>, false>>, ]

又是一道考验字符串操作的题:

typescript
type StartsWith<T extends string, U extends string> = T extends `${U}${infer R}` ? true : false

EndsWith

Implement EndsWith<T, U> which takes two exact string types and returns whether T ends with U

typescript
type EndsWith<T extends string, U extends string> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<EndsWith<'abc', 'bc'>, true>>, Expect<Equal<EndsWith<'abc', 'abc'>, true>>, Expect<Equal<EndsWith<'abc', 'd'>, false>>, ]

和 StartsWith 一样,只需要变换一下位置:

typescript
type EndsWith<T extends string, U extends string> = T extends `${infer R}${U}` ? true : false

PartialByKeys

Implement a generic PartialByKeys<T, K> which takes two type argument T and K.

typescript
type PartialByKeys<T, K> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' interface User { name: string age: number address: string } interface UserPartialName { name?: string age: number address: string } interface UserPartialNameAndAge { name?: string age?: number address: string } type cases = [ Expect<Equal<PartialByKeys<User, 'name'>, UserPartialName>>, Expect<Equal<PartialByKeys<User, 'name' | 'unknown'>, UserPartialName>>, Expect<Equal<PartialByKeys<User, 'name' | 'age'>, UserPartialNameAndAge>>, Expect<Equal<PartialByKeys<User>, Partial<User>>>, ]

将符合指定的 Key 的那些项转成可选项,我们可以分成两步来做:一是从源对象中移除指定的 Key(Omit);二是从源对象中取出指定的 Key (Extract)并将它们转成可选的:

typescript
// 1. 通过 Omit 移除指定的 Key type RequiredObject<T, K extends PropertyKey = keyof T> = Omit<T, K> // 2. 通过 Extract 过滤指定的 Key type PatrialObject<T, K extends PropertyKey = keyof T> = { [P in Extract<K, keyof T>]?: T[P] }

然后再将两者合并即可:

typescript
type Merge<T> = Omit<T, never> type PartialByKeys<T, K extends PropertyKey = keyof T> = Merge<Omit<T, K> & { [P in Extract<K, keyof T>]?: T[P] }>

RequiredByKeys

Implement a generic RequiredByKeys<T, K> which takes two type argument T and K.

K specify the set of properties of T that should set to be required. When K is not provided, it should make all properties required just like the normal Required<T>.

typescript
type RequiredByKeys<T, K> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' interface User { name?: string age?: number address?: string } interface UserRequiredName { name: string age?: number address?: string } interface UserRequiredNameAndAge { name: string age: number address?: string } type cases = [ Expect<Equal<RequiredByKeys<User, 'name'>, UserRequiredName>>, Expect<Equal<RequiredByKeys<User, 'name' | 'unknown'>, UserRequiredName>>, Expect<Equal<RequiredByKeys<User, 'name' | 'age'>, UserRequiredNameAndAge>>, Expect<Equal<RequiredByKeys<User>, Required<User>>>, ]

这题的思路和 PartialByKeys 一样,但有点小区别:

typescript
type Merge<T> = Omit<T, never> type RequiredByKeys<T, K = keyof T> = Merge<{ [P in keyof T as P extends K ? P : never]-?: T[P] } & { [P in keyof T as P extends K ? never : P]: T[P] }>

Mutable

Implement the generic Mutable<T> which makes all properties in T mutable (not readonly).

typescript
type Mutable<T> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' interface Todo1 { title: string description: string completed: boolean meta: { author: string } } type List = [1, 2, 3] type cases = [ Expect<Equal<Mutable<Readonly<Todo1>>, Todo1>>, Expect<Equal<Mutable<Readonly<List>>, List>>, ] type errors = [ // @ts-expect-error Mutable<'string'>, // @ts-expect-error Mutable<0>, ]

通过 - 号操作可以把 readonly 标识给移除,同时别忘了给泛型加约束处理掉 errors:

typescript
type Mutable<T extends Record<PropertyKey, any>> = { -readonly [P in keyof T]: T[P] }

OmitByType

From T, pick a set of properties whose type are not assignable to U.

typescript
type OmitByType<T, U> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' interface Model { name: string count: number isReadonly: boolean isEnable: boolean } type cases = [ Expect<Equal<OmitByType<Model, boolean>, { name: string; count: number }>>, Expect<Equal<OmitByType<Model, string>, { count: number; isReadonly: boolean; isEnable: boolean }>>, Expect<Equal<OmitByType<Model, number>, { name: string; isReadonly: boolean; isEnable: boolean }>>, ]

只需要把值的类型做一次对比即可:

typescript
type OmitByType<T, U> = { [P in keyof T as T[P] extends U ? never : P]: T[P] }

ObjectEntries

Implement the type version of Object.entries

typescript
type ObjectEntries<T> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' interface Model { name: string age: number locations: string[] | null } type ModelEntries = ['name', string] | ['age', number] | ['locations', string[] | null] type cases = [ Expect<Equal<ObjectEntries<Model>, ModelEntries>>, Expect<Equal<ObjectEntries<Partial<Model>>, ModelEntries>>, Expect<Equal<ObjectEntries<{ key?: undefined }>, ['key', undefined]>>, Expect<Equal<ObjectEntries<{ key: undefined }>, ['key', undefined]>>, ]

这题需要注意两点,一个是把 T 转成 Required,另一个就是 undefined 的处理:

typescript
type ObjectEntries<T, R = Required<T>, K extends keyof R = keyof R> = K extends keyof R ? [K, R[K] extends undefined ? undefined : R[K]] : never

Shift

Implement the type version of Array.shift

typescript
type Shift<T> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<Shift<[3, 2, 1]>, [2, 1]>>, Expect<Equal<Shift<['a', 'b', 'c', 'd']>, ['b', 'c', 'd']>>, ]

这题是比较简单的:

typescript
type Shift<T> = T extends [infer F, ...infer R] ? R : never

Tuple to Nested Object

Given a tuple type T that only contains string type, and a type U, build an object recursively.

typescript
type TupleToNestedObject<T, U> = any /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<TupleToNestedObject<['a'], string>, { a: string }>>, Expect<Equal<TupleToNestedObject<['a', 'b'], number>, { a: { b: number } }>>, Expect<Equal<TupleToNestedObject<['a', 'b', 'c'], boolean>, { a: { b: { c: boolean } } }>>, Expect<Equal<TupleToNestedObject<[], boolean>, boolean>>, ]

常规的数组递归操作即可:

typescript
type TupleToNestedObject<T, U> = T extends [infer F extends string, ...infer R] ? { [P in F]: TupleToNestedObject<R, U> } : U