TypeScript 类型体操练习笔记(三)

进度(145 /188)

其中标记 ※ 的是我认为比较难或者涉及新知识点的题目

刷题也许没有什么意义,但是喜欢一个人思考一整天的灵光一现,也喜欢看到新奇的答案时的恍然大悟,仅此而已。

系列文

91. Medium - 18142 - All

如果传入的第一个参数中所有元素都等于传入的第二个参数,则返回 true;如果有不匹配,则返回 false

ts 复制代码
// 遍历
type All<T extends any[], U> =
  T extends [infer F, ...infer R]
    ? Equal<F, U> extends true
      ? All<R, U>
      : false
    : true

// 利用联合类型的分布式
type All<T extends any[], N, U = T[number]> =
  false extends (U extends any ? Equal<U, N> : never) ? false : true

92. Medium - 18220 - Filter

实现类型 Filter<T, Predicate> 接受一个数组类型 T,原始类型或者原始类型的联合类型 P 返回 T 中包含 P 的元素组成的数组。

ts 复制代码
type Filter<T extends any[], P> = 
  T extends [infer F, ...infer R]
    ? F extends P
      ? [F, ...Filter<R, P>]
      : Filter<R, P>
    : []

93. Medium - 21104 - FindAll

给定一个模式字符串 P 和一个文本字符串 T,实现类型 FindAll<T,P>,该类型返回一个数组,其中包含 P 匹配的 T 中的所有索引(从 0 开始的索引)。

ts 复制代码
type FindAll<
  T extends string,
  P extends string,
  Index extends any[] = [],
  Res extends number[] = [],
> = P extends ""
  ? []
  : T extends `${infer _F}${infer R}`
    ? T extends `${P}${infer _}`
      ? FindAll<R, P, [...Index, 0], [...Res, Index["length"]]>
      : FindAll<R, P, [...Index, 0], Res>
    : Res;

94. Medium - 21106 - Combination key type

  1. 把多个修饰键两两组合,但不可以出现相同的修饰键组合。
  2. 提供的 ModifierKeys 中,前面的值比后面的值高,即 cmd ctrl 是可以的,但 ctrl cmd 是不允许的。
ts 复制代码
type Combs<T extends string[], U extends string = T[number]> =
  T extends [infer F extends string, ...infer R extends string[]]
    ? U extends any
      ? F extends U
        ? never
        : `${F} ${U}` | Combs<R>
      : never
    : never

95. Medium - 21220 - Permutations of Tuple

给定一个泛型元组类型 T extends unknown[],编写一个类型生成 T 的全排列作为一个并集。

ts 复制代码
type Join<U extends any[], F> = U extends any ? [F, ...U] : []

// [...T,...Pre]的全排列中以T中每个元素开头的部分
type PermutationsOfTuple<
  T extends unknown[],
  Pre extends any[] = []
> = T extends [infer F, ...infer R]
  ?
      | Join<PermutationsOfTuple<[...R, ...Pre]>, F>
      | (R['length'] extends 0 ? never : PermutationsOfTuple<R, [...Pre, F]>)
  : []

这种题真恶心吐了,想了一下午。

96. Medium - 25170 - Replace First

实现类型 ReplaceFirst<T, S, R> 把元组 T 中第一个 S 替换为 R

ts 复制代码
type ReplaceFirst<T extends readonly unknown[], S, R, Pre extends any[] = []> =
  T extends [infer F, ...infer Rest]
    ? F extends S
      ? [...Pre, R, ...Rest]
      : ReplaceFirst<Rest, S, R, [...Pre, F]>
    : Pre

97. Medium - 25270 - Transpose ※

矩阵的转置是一种将矩阵翻转到其对角线上的运算符;也就是说,它通过产生另一个矩阵(通常用 <math xmlns="http://www.w3.org/1998/Math/MathML"> A T A^T </math>AT表示)来转换矩阵A的行和列索引。

ts 复制代码
// 把T中每一个子数组在下标Index对应的元素组成一个新数组
type PushAllByIndex<
  T extends any[],
  Index extends any[],
  Res extends any[] = [],
> = T extends [infer F extends any[], ...infer R extends any[]]
  ? PushAllByIndex<R, Index, [...Res, F[Index["length"]]]>
  : Res;

/**
 * 翻转矩阵
 * 把原数组中所有数组的第0个组成一个新数组,所有数组的第1个组成一个新数组,...
 */
type Transpose<
  M extends number[][],
  Res extends number[][] = [],
  Index extends any[] = [],
> = Index['length'] extends M[0]["length"] // 遍历所有的index 
  ? Res
  : Transpose<M, [...Res, PushAllByIndex<M, Index>], [...Index, 0]>;

有点烧脑的,我加了一个辅助类型 PushAllByIndex

在评论区看到一个精彩的解法 #25297

ts 复制代码
type Transpose<M extends number[][], R = M["length"] extends 0 ? [] : M[0]> = {
  [X in keyof R]: {
    [Y in keyof M]: X extends keyof M[Y] ? M[Y][X] : never
  }
}

利用映射类型进行两层遍历,取 R 为原矩阵第一行,如果 M 为空数组,则为 []。然后先遍历 R 也是遍历原矩阵的每一列,再遍历 M 也就是每一行,再根据下标取逆转前对应的元素。

如果直接按下面这么写,会有类型报错,所以加了一层类型约束。

ts 复制代码
...{
  [X in keyof R]: {
    [Y in keyof M]: M[Y][X] // Type 'X' cannot be used to index type 'M[Y]'.
  }
}

98. Medium - 26401 - JSON Schema to TypeScript

实现泛型类型 JSONSchema2TS,它返回与给定 JSON schema 对应的 TypeScript 类型。

ts 复制代码
type JSONSchema = {
  type: string
  enum?: any[]
  properties?: Record<string, JSONSchema>
  required?: string[]
  items?: JSONSchema
}
type IntersectionToObj<T> = {
  [K in keyof T]: T[K]
}
type RequiredByKeys<T, K extends keyof T = keyof T> = IntersectionToObj<
  Omit<T, K> & Required<Pick<T, K>>
>

type JSONSchema2TS<T extends JSONSchema> = 
T['enum'] extends any[] // 先判断是否为枚举类型
  ? T['enum'][number] // 如果是枚举类型直接返回对应枚举
  : T['type'] extends 'string' // 判断是否为字符串类型
    ? string
    : T['type'] extends 'number' // 判断是否为数字类型
      ? number
      : T['type'] extends 'boolean' // 判断是否为布尔类型
        ? boolean
        : T['type'] extends 'array' // 判断是否为数组类型
          ? T['items'] extends JSONSchema // 判断是否制定了数组元素类型
            ? JSONSchema2TS<T['items']>[] // 如果指定了子元素类型,先递归解析出子元素类型,然后构造数组
            : unknown[] // 没有指定子类型,直接返回 unknown[]
          : T['type'] extends 'object' // 判断是否为对象类型
            ? T['properties'] extends Record<string, JSONSchema> // 是否指定了对象属性
              ? T['required'] extends string[] // 是否指定了required属性
                // 指定了required属性,利用之前的RequiredByKeys类型,把对应属性变成required
                ? RequiredByKeys<
                    {
                      [K in keyof T['properties']]?: JSONSchema2TS<
                        T['properties'][K]
                      >
                    },
                    T['required'][number]
                  >
                : { // 没有指定required,所有属性都是可选的
                    [K in keyof T['properties']]?: JSONSchema2TS<
                      T['properties'][K]
                    >
                  }
              : Record<string, unknown> // 没有指定属性,返回Record<string, unknown>
            : never

枚举+递归,不难,但是分支有点多。利用了之前实现的 RequiredByKeys 工具类型。

99. Medium - 27133 - Square ※

给一个数字,返回它的平方。

目前为止,遇到的最恶心的一道题,我简直写了一套大数模拟算法。

ts 复制代码
/**
 * 反转字符串
 */
type Reverse<T extends string | number> = `${T}` extends `${infer F}${infer R}`
  ? `${Reverse<R>}${F}`
  : ''

/**
 * 得到一个指定长度的元组
 */
type GetTuple<N> =
  N extends '0' ? [] :
  N extends '1' ? [0] :
  N extends '2' ? [0, 0] :
  N extends '3' ? [0, 0, 0] :
  N extends '4' ? [0, 0, 0, 0] :
  N extends '5' ? [0, 0, 0, 0, 0] :
  N extends '6' ? [0, 0, 0, 0, 0, 0] :
  N extends '7' ? [0, 0, 0, 0, 0, 0, 0] :
  N extends '8' ? [0, 0, 0, 0, 0, 0, 0, 0] :
  N extends '9' ? [0, 0, 0, 0, 0, 0, 0, 0, 0] : []

/**
 * 计算个位数字A+B+C是否会进位(C=0/1)
 */
type GetAddCarry<A extends string, B extends string, C extends string = '0'> = [
  ...GetTuple<A>,
  ...GetTuple<B>,
  ...GetTuple<C>,
][9] extends 0
  ? '1'
  : '0'

/**
 * 计算个位数字A+B+C的个位数(C=0/1)
 */
type GetAddDigit<
  A extends string,
  B extends string,
  C extends string = '0',
  AddTuple extends number[] = [...GetTuple<A>, ...GetTuple<B>, ...GetTuple<C>],
> = AddTuple[9] extends 0
  ? `${AddTuple['length']}` extends `1${infer R}`
    ? R
    : 0
  : `${AddTuple['length']}`

/**
 * 计算A+B(翻转后)
 */
type AddUtil<
  A,
  B,
  Carry extends '0' | '1' = '0',
  Result extends string = '',
> = A extends `${infer AFirst}${infer ARest}`
  ? B extends `${infer BFirst}${infer BRest}`
    ? AddUtil<
        ARest,
        BRest,
        GetAddCarry<AFirst, BFirst, Carry>,
        `${GetAddDigit<AFirst, BFirst, Carry>}${Result}`
      >
    : AddUtil<
        ARest,
        '',
        GetAddCarry<AFirst, '0', Carry>,
        `${GetAddDigit<AFirst, '0', Carry>}${Result}`
      >
  : B extends `${infer BFirst}${infer BRest}`
    ? AddUtil<
        '',
        BRest,
        GetAddCarry<'0', BFirst, Carry>,
        `${GetAddDigit<'0', BFirst, Carry>}${Result}`
      >
    : Carry extends '1'
      ? `1${Result}`
      : Result

/**
 * 计算 A+B
 */
type Add<A extends number, B extends number> = AddUtil<Reverse<A>, Reverse<B>>

/**
 * 把字符串转成数字
 */
type String2Number<S> = S extends `${infer N extends number}` ? N : 0

// 计算 X*N
type Multi<
  X extends number,
  N extends number,
  Index extends any[] = [],
  Res extends number = 0,
> = Index['length'] extends N
  ? Res
  : Multi<X, N, [...Index, 0], String2Number<Add<Res, X>>>

/**
 * 取数字绝对值
 */
type Abs<N extends number> = `${N}` extends `-${infer Num extends number}` ? Num : N

/**
 * 利用乘法计算平方
 */
type Square<N extends number> = Multi<Abs<N>, Abs<N>>

type Answer = Square<999> // 998001

100. Medium - 27152 - Triangular number

给定一个数字 N,找到第 N 个三角形数,即 1+2+3+...+N

ts 复制代码
type Triangular<
  N extends number, // 传入的参数N
  Cur extends any[] = [], // 当前Index对应的答案
  Index extends any[] = [] // 计算到第几个数字
> = Index['length'] extends N // 从0开始计算,是否计算到了N
  ? Cur['length'] // Cur的长度记录答案
  : Triangular<N, [...Cur, ...Index, 0], [...Index, 0]>; // f(n+1)=f(n)+(n+1)

101. Medium - 27862 - CartesianProduct

给定两个集合(联合),返回其笛卡尔积的元组集合。分布式条件类型秒了。

ts 复制代码
type CartesianProduct<T, U> = T extends any
  ? U extends any
    ? [T, U]
    : never
  : never;

102. Medium - 27932 - MergeAll ※

将可变数量的类型合并到一个新类型中。如果键重叠,则其值应合并为一个联合。

ts 复制代码
type MergeAll<XS, Result extends Record<string, any> = {}> = XS extends [
  infer F,
  ...infer R
]
  ? MergeAll<
      R,
      {
        [K in keyof F | keyof Result]: K extends keyof F
          ? K extends keyof Result
            ? F[K] | Result[K]
            : F[K]
          : K extends keyof Result
          ? Result[K]
          : never;
      }
    >
  : Result;

最简单的方案,递归依次 Merge 每一个元素。

然后尝试一次出结果,我开始是这么写的,但是明显不对,U 是联合类型,在映射类型中会触发分布式。

ts 复制代码
type MergeAll<XS extends any[], U = XS[number]> = {
  [K in keyof U]: U[K]
}
type exp = MergeAll<[{ a: string }, { b: string }]>
// MergeAll<[{
//   a: string;
// }, {
//   b: string;
// }], {
//   a: string;
// }> | MergeAll<[{
//   a: string;
// }, {
//   b: string;
// }], {
//   b: string;
// }>

所以需要提前拿到键的集合

ts 复制代码
type MergeAll<
  XS extends any[],
  U = XS[number],
  Keys extends PropertyKey = U extends U ? keyof U : never
> = {
  [K in Keys]: U extends U ? (K extends keyof U ? U[K] : never) : never;
};

其中 (K extends keyof U ? U[K] : never) 是为了限制 K 只访问对象实际存在的键。可以简化成下面的形式,U[K & keyof U]

ts 复制代码
type MergeAll<
  XS extends object[],
  U = XS[number],
  Keys extends PropertyKey = U extends U ? keyof U : never
> = {
  [K in Keys]: U extends U ? U[K & keyof U] : never
}

103. Medium - 27958 - CheckRepeatedTuple

实现类型 CheckRepeatedChars<T> 返回元组类型 T 中是否有相同的成员。

ts 复制代码
type Includes<T extends unknown[], U> = 
  T extends [infer F, ...infer R] 
    ? Equal<U, F> extends true 
      ? true 
      : Includes<R, U>
    : false

type CheckRepeatedTuple<T extends unknown[]> = 
  T extends [infer F, ...infer Rest]
    ? Includes<Rest, F> extends true
      ? true
      : CheckRepeatedTuple<Rest>
    : false

总感觉这题做过,放在这里没啥意思。本来想用联合类型作为一个 Set 来判断的,但是有这种测试用例: [boolean, true, false]boolean 在分布式条件类型会被分布为 falsetrue,所以就两层循环判断了。

ts 复制代码
type TestUnion<U> = U extends U ? [U] : never
type R = TestUnion<boolean> // [false] | [true]

104. Medium - 28333 - Public Type

移除类型 T 中所有以 _ 开头的属性。

ts 复制代码
type PublicType<T extends object> = {
  [K in keyof T as K extends `_${string}` ? never : K]: T[K]
}

涉及知识点:映射类型中使用 as 重命名键名和模板字符串类型。

105. Medium - 29650 - ExtractToObject

实现一个将指定属性 Prop 的值提取到接口的类型。该类型接受两个参数。输出应该是一个具有 Prop 值的对象。Prop 的值是对象。

ts 复制代码
type ExtractToObject<T, U extends keyof T> = {
  [K in keyof T | keyof T[U] as K extends U ? never : K]:
    K extends keyof T
      ? T[K]
      : K extends keyof T[U]
        ? T[U][K]
        : never
}
// 或者利用Omit
type ExtractToObject<T, U extends keyof T> = Omit<Omit<T, U> & T[U], never>

106. Medium - 29785 - Deep Omit

实现 DeepOmit, 类似于工具类型 Omit,可以删除多层子属性。

ts 复制代码
type DeepOmit<T, Props> =
  Props extends `${infer L}.${infer R}`
    ? Omit<Omit<T, L> & Record<L, DeepOmit<T[L & keyof T], R>>, never>
    : Omit<T, Props & keyof T>

107. Medium - 30301 - IsOdd

判断一个数字是否为奇数。注意到测试用例有 2.33e23,前者是小数,不可能是奇数,后者是大数,所以用到了 bigint 它可以匹配大整数,不支持小数。如果用 number 的话,它会匹配小数。

ts 复制代码
type IsOdd<T extends number> = 
  `${T}` extends `${bigint  |''}${1 | 3 | 5 | 7 | 9}` 
    ? true 
    : false;

108. Medium - 30430 - Tower of hanoi

模拟汉诺塔谜题的解决方案,将环的数量作为输入,并返回一系列步骤,将环从塔 A 移动到塔 B,并使用塔 C 作为辅助。数组中的每个条目都应该是一对字符串 [From,To],表示环被移动 From -> To

ts 复制代码
// 处理 N-Index['length']
type Hanoi<
  N extends number,
  From = 'A',
  To = 'B',
  Intermediate = 'C',
  Index extends number[] = []
> = Index['length'] extends N
  ? []
  : [
      ...Hanoi<N, From, Intermediate, To, [...Index, 0]>,
      [From, To],
      ...Hanoi<N, Intermediate, To, From, [...Index, 0]>
    ];

109. Medium - 30958 - Pascal's triangle

给一个数字,构造 Pascal's triangle(帕斯卡三角形、杨辉三角)。 Wikipedia

ts 复制代码
// 生成一个长度为N的元组,之前实现过复杂的,这里数据小,写个简单的
type GetNTuple<N, Index extends any[] = []> = Index['length'] extends N ? Index : GetNTuple<N, [...Index, 0]>
// 计算两数之和
type Add<A, B> = [...GetNTuple<A>, ...GetNTuple<B>]['length']
// 根据上一行生成下一行,每个数字是上一行当前位置的数字+前一个数字,最后再加一个1
type NextLine<Row extends any[], Pre = 0, Res extends any[] = []> =
 Row extends [infer L, ...infer R]
    ? NextLine<R, L, [...Res, Add<L, Pre>]>
    : [...Res, 1]
// 生成高度为N的三角 Index+1表示当前计算到的下标 计算到N-1的时候就可以返回结果了
type Pascal<N extends number, Index extends any[] = [], Res extends any[] = [[1]]> =
  [...Index, 0]['length'] extends N // 计算到了所需要的高度,返回结果
    ? Res
    : Pascal<N, [...Index, 0], [...Res, NextLine<Res[Index['length']]>]>

110. Medium - 30970 - IsFixedStringLiteralType ※

有时你可能想要判断一个字符串字面量是否是确定的类型。例如,当你想检查作为类标识符指定的类型是否是固定的字符串字面量类型时。(下面例子中,ID需要为一个确定的类型,string 约束太宽松)

ts 复制代码
type Action<ID extends string> = { readonly id: ID };

由于它必须是固定的,以下类型必须被判定为 false

  • never 类型
  • 字符串字面量类型的联合类型
  • 包含嵌入 string、number、bigint、boolean 的模板字面量类型

请判断给定的类型 S 是否是一个确定的字符串字面量类型。

我洋洋洒洒写了几十行,结果看到大佬的答案,极其优雅 #33984

ts 复制代码
type IsFixedStringLiteralType<S extends string> =
    {} extends Record<S, 1>
        ? false
        : Equal<[S], S extends unknown ? [S] : never>

仔细分析一下 {} extends Record<S, 1> 所有情况:

  • 如果 S 是有限的键,在映射类型中,每个属性都是 required ,此时 {} extends Record<S, 1>false

    ts 复制代码
    Record<`${boolean}`, 1> // { false: 1; true: 1; }
    Record<`${undefined}`, 1> // { undefined: 1; }
    Record<`ABC`, 1> // { ABC: 1; }
  • 如果 Snever 那么 Record<S, 1> 就是 {}

  • 如果 S 是无限的集合,那就是索引签名,此时相当于每个键都是 optional 所以 {} 可以赋值给它。

    ts 复制代码
    Record<`${number}`, 1> //  [x: `${number}`]: 1;
    Record<`${string}`, 1> //  [x: string]: 1;

太精彩了!

第二句 Equal<[S], S extends unknown ? [S] : never> 筛除了联合类型。

111. Medium - 34007 - Compare Array Length

实现 CompareArrayLength 以比较两个数组的长度。

T 数组长度大于 U,返回 1;若 U 数组长度大于 T,返回 -1;若 T 数组长度等于 U,返回 0。

ts 复制代码
type CompareArrayLength<
  T extends any[],
  U extends any[]
> = T['length'] extends U['length']
  ? 0
  : keyof U extends keyof T ? 1 : -1;

112. Medium - 34857 - Defined Partial Record

生成 T 所有非空属性子集的联合类型。

ts 复制代码
type DefinedPartial<T, K extends keyof T = keyof T> = K extends any
  ? T | DefinedPartial<Omit<T, K>>
  : never;

解析:举例对于 {a, b, c} 其所有非空子集是:{a,b,c}, {a,b}, {a,c}, {b,c}, {a}, {b}, {c}

而对于每个元素 K,可以选择"去掉它"或"保留它"

<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> 所有子集 = 包含 K 的子集 ∪ 不包含 K 的子集 所有子集 = 包含 K 的子集 ∪ 不包含 K 的子集 </math>所有子集=包含K的子集∪不包含K的子集

通过分布式条件类型来枚举子属性 K

113. Medium - 35045 - Longest Common Prefix

实现类型 LongestCommonPrefix 返回元组的最长公共前缀,如果没有公共前缀,返回空字符串 ""

ts 复制代码
type LongestCommonPrefix<
  T extends string[],
  P extends string = '',
  U = T[number]
> = T[0] extends `${P}${infer N}${string}`
  ? false extends (U extends `${P}${N}${string}` ? true : false)
    ? P
    : LongestCommonPrefix<T, `${P}${N}`>
  : P;

P 作为 T[0] 的前缀,不断增加长度,利用分布式一次判断 P+下一个字符N 是否能匹配元组中所有字符串,如果可以就继续递归处理。否则返回当前最长公共前缀 P

114. Medium - 35191 - Trace

方阵的迹(Trace)是指其主对角线元素的和。然而,使用类型系统难以计算该和。为简化处理,我们可将主对角线元素返回为联合类型。

ts 复制代码
type Trace<
  T extends any[][],
  Index extends any[] = []
> = Index['length'] extends T['length']
  ? never
  : T[Index['length']][Index['length']] | Trace<T, [...Index, 0]>;

还有更天才的解法,通过映射类型来遍历下标生成一个对象,值为下标对应元素 T[Index][Index],然后再通过 [number] 取所有元素的联合。

ts 复制代码
type Trace<T extends any[][]> = {
  [Index in keyof T]: T[Index][Index & keyof T[Index]];
}[number];

115. Medium - 35252 - IsAlphabet

判断给定的字符是否为字母。

ts 复制代码
type IsAlphabet<S extends string> = Uppercase<S> extends Lowercase<S> ? false : true;

利用了内置工具类型,太妙了。

Hard

终于做到了 Hard :D

116. Hard - 6 - Simple Vue ※

实现类似Vue的类型支持的简化版本。

ts 复制代码
type GetComputed<C> = C extends Record<string, (...args: any[]) => any>
  ? { [K in keyof C]: ReturnType<C[K]> }
  : never;

declare function SimpleVue<D, C, M>(options: {
  data: (this: void) => D;
  computed: C & ThisType<D>;
  methods: M & ThisType<D & GetComputed<C> & M>;
}): unknown;

这里学习了一个新知识 ThisType<T>

在默认情况下,一个对象字面量内部函数的 this 指向的是这个对象本身 。但在很多库(如 Vue、Pinia、JQuery)中,框架会通过 callapply 动态改变 this 的指向。

ThisType<T> 就是为了解决这个"类型断层"而生的。它告诉编译器:"在这个对象字面量里,不管原本 this 该是谁,现在请把它看作 T。"

如果你定义一个普通对象,this 只能看到对象里已有的东西。

ts 复制代码
const obj = {
  x: 10,
  getX() {
    return this.x; // 正常,this 指向 obj
  },
  getY() {
    return this.y; // 报错:Property 'y' does not exist
  }
}

可以手动指定 this 参数,注意 this 必须是第一个参数:

ts 复制代码
function Example(
  this: { name: string }, // 伪参数,指定 this 的类型
  age: number,            // 真正的第 1 个参数
  city: string            // 真正的第 2 个参数
) {
  console.log(`${this.name} is ${age} years old and lives in ${city}.`);
}

当你手动指定了 this,你不能直接像普通函数那样调用它(除非调用者的上下文正好符合要求),通常需要使用 .call().apply().bind()

ts 复制代码
// 直接调用会报错,因为 TypeScript 认为没有提供符合要求的 this
// Example(25, "Beijing"); // Error

const user = { name: "Alice" };

// 正确调用方式:
Example.call(user, 25, "Beijing");
Example.apply(user, [25, "Beijing"]);

const boundExample = Example.bind(user);
boundExample(25, "Beijing");

在对象/类方法中使用:如果你在对象方法里手动指定 this,它会覆盖 TS 默认推导的类型。

ts 复制代码
interface DB {
  filter(
    this: string[], // 强制要求 this 必须是一个字符串数组
    callback: (val: string) => boolean
  ): string[];
}

const myDb: DB = {
  filter(callback) {
    // 这里的 this 被限定为 string[]
    return this.filter(callback); 
  }
};

// 错误用法:
// const wrong = { filter: myDb.filter, x: 1 };
// wrong.filter(s => s.length > 0); // 报错:The 'this' context... is not assignable to 'string[]'

通过 ThisType<T> 注入 this 的类型。

ts 复制代码
interface MyContext {
  x: number;
  log: (msg: string) => void;
}

// 通过 & ThisType<MyContext>,我们强行覆盖了 desc 内部的 this 类型
const desc: { data: string, setup: Function } & ThisType<MyContext> = {
  data: "hello",
  setup() {
    this.x = 10;          // 正确,MyContext.x: number
    this.log("success");  // 正确,MyContext.log: (msg: string) => void
    console.log(this.data); // 错误,Property 'data' does not exist on type 'MyContext'.
  }
};

desc.data = 'data' // 正确 data: string
desc.x = 10 // 错误 Property 'x' does not exist

当使用 let obj: Type & ThisType<Context> 时:

  1. 成员检查obj 外部依然遵循 Type 的约束(你不能给 obj 随便赋值 Context 里的属性)。
  2. 上下文推导 :当编译器进入 obj 内部的函数体时,它会跳过默认的推导逻辑,直接查阅 ThisType 提供的类型。

回到本题,根据 Vue 的定义,我们需要把 datamethodscomputed 的值都放进 this 其中 computed 是函数,但是使用的时候是计算后的对象,所以需要取函数的返回值。

117. Hard - 17 - Currying 1 - 柯里化 1 ※

柯里化 是一种将带有多个参数的函数转换为每个带有一个参数的函数序列的技术。

ts 复制代码
type Curried<Fn> = Fn extends (...args: infer P) => infer R
  ? P['length'] extends 1 | 0 // 如果函数只有一个参数或者没有参数,不需要处理
    ? Fn
    : P extends [infer F, ...infer O]
      ? (a: F) => Curried<(...args: O) => R>
      : R
  : Fn;

declare function Currying<Fn>(fn: Fn): Curried<Fn>;

每次提取第一个参数类型,然后用剩下的参数递归处理。

118. Hard - 55 - Union to Intersection 联合类型转化为交叉类型 ※

实现高级工具类型 UnionToIntersection<U> 把联合类型改为交叉类型。

ts 复制代码
type UnionToIntersection<U> = (
  U extends any ? (args: U) => any : never
) extends (args: infer R) => any
  ? R
  : never;

这里涉及到一个新的知识点 ------ TypeScript 的逆变与协变:

在 TypeScript 中,函数的返回值是协变 的,而函数的参数是逆变的。

  • 协变 (Covariant) :如果 AB 的子集,那么 () => A 也是 () => B 的子集。
  • 逆变 (Contravariant) :如果 AB 的子集,那么 (arg: B) => void 才是 (arg: A) => void 的子集(方向反过来了)。

当编译器在逆变位置 (即函数参数位)遇到同一个类型变量 R 的多个候选类型时,为了保证类型安全,它会推导这些候选类型的交叉类型

ts 复制代码
type UnionToFunction<U> = U extends any ? (args: U) => any : never;
// 把联合类型转换为函数的联合类型
// UnionToFunction<'foo' | 42 | true>;
// ((args: true) => any) | ((args: 42) => any) | ((args: "foo") => any)

type UnionToIntersection<U> = UnionToFunction<U> extends (args: infer P) => any
  ? P
  : never;
// 根据TypeScript逆变,P需要符合前面所有的函数参数,需要是前面所有函数参数的交叉类型。

119. Hard - 57 - Get Required 获得必需的属性 ※

实现高级工具类型 GetRequired<T>,该类型保留所有必需的属性。

ts 复制代码
type GetRequired<T> = {
  [K in keyof T as T[K] extends Required<T>[K] ? K : never]: T[K]
}

// 或者

type GetRequired<T> = {
  [K in keyof T as (
    {} extends Pick<T, K> ? never : K // 如果 {} 能分配给 Pick<T, K>,说明 K 是可选的
  )]: T[K]
}

在 TypeScript 中 foo: undefinedfoo?: undefined 是不一样的,前者属性必须存在,后者则可以不存在。但是我们没办法显式处理 ? 这种 flag 只能通过上面的方案来判断。

120. Hard - 59 - Get Optional 获得可选属性

实现高级工具类型 GetOptional<T>,该类型保留所有可选属性

和上面的题反过来即可。

ts 复制代码
type GetOptional<T> = {
  [K in keyof T as {} extends Pick<T, K> ? K : never]: T[K]
}

121. Hard - 89 - Required Keys 必需的键

实现高级工具类型 RequiredKeys<T>,该类型返回 T 中所有必需属性的键组成的一个联合类型。

ts 复制代码
type RequiredKeys<T> = keyof GetRequired<T>

利用前面的 GetRequired

122. Hard - 90 - Optional Keys 可选类型的键

实现高级工具类型OptionalKeys<T>,该类型将 T 中所有可选属性的键合并为一个联合类型。

ts 复制代码
type OptionalKeys<T> = keyof GetOptional<T>

123. Hard - 112 - Capitalize Words

实现 CapitalizeWords<T>,它将字符串的每个单词的第一个字母转换为大写,其余部分保持原样。

ts 复制代码
type CapitalizeWords<
  S extends string,
  W extends string = ''
> = S extends `${infer L}${infer R}`
  ? Uppercase<L> extends Lowercase<L> // 当前字符是否为字母
    ? `${Capitalize<`${W}${L}`>}${CapitalizeWords<R, ''>}` // 不是字母,一个单词已经结束了,把前面的单词首字母大写,接下处理下一个单词
    : CapitalizeWords<R, `${W}${L}`> // 是字母,这个字符和前面的字符连接起来
  : Capitalize<W> // 字符串处理完了,把最后一个单词大写

很奇怪的一件事,我开始是这么写的,每个字符分别处理,Up 记录前一个字符是否不是字母,如果不是则下一个字符需要被大写:

ts 复制代码
type CapitalizeWords<
  S extends string,
  Up extends boolean = true
> = S extends `${infer L}${infer R}`
  ? Uppercase<L> extends Lowercase<L>
    ? `${L}${CapitalizeWords<R, true>}`
    : Up extends true
      ? `${Uppercase<L>}${CapitalizeWords<R, false>}`
      : `${L}${CapitalizeWords<R, false>}`
  : S;

但是这个 case 出现了错误,我研究了很久,我的依赖版本是 "typescript": "^5.3.2",好像其他版本并没有问题。很奇怪。

ts 复制代码
CapitalizeWords<" Qq"> // `\uD83E\uDD23${any}${any}`

124. Hard - 114 - CamelCase

实现 CamelCase<T> ,将 snake_case 类型的表示的字符串转换为 camelCase 的表示方式。

ts 复制代码
type CamelCase<
  S extends string,
  Up extends boolean = false
> = S extends `${infer L}${infer R}` // 取S第一个字符L
  ? L extends '_' // 判断L是否为下划线
    ? R extends `${infer R1}${infer _}` // 取得下划线的下一个字符
      ? Lowercase<R1> extends Uppercase<R1> // 下划线后面接字母才处理,判断是否为字母
        ? `_${CamelCase<R, false>}` // 下一个字符不是字母,保留下划线
        : CamelCase<R, true> // 下一个字符是字母,删除下划线,下一个字母大写
      : '_' // 下划线后面没有其他字符了,保留下划线
    : // 当前字符不是下划线,根据Up判断是否需要大写
      `${Up extends true ? Uppercase<L> : Lowercase<L>}${CamelCase<R, false>}`
  : '' // S为空串直接返回空

125. Hard - 147 - C-printf Parser

按照C语言的 printf 的格式来处理字符串中的 %d%f 为元组 ['dec'], ['float'] 等。

ts 复制代码
type ParsePrintFormat<S> = S extends `${string}%${infer B}${infer C}`
  ? B extends keyof ControlsMap
    ? [ControlsMap[B], ...ParsePrintFormat<C>]
    : ParsePrintFormat<C>
  : []

126. Hard - 213 - Vue Basic Props ※

基于 6 - Simple Vue 实现带 props 的 Vue 类型。

ts 复制代码
type GetComputed<T extends Record<string, any>> = {
  [K in keyof T]: ReturnType<T[K]>
}

type ToPrimitive<T> = T extends {
  (): infer R
}
  ? R
  : T extends RegExpConstructor
    ? RegExp
    : T extends {
        new (): infer A
      }
      ? A
      : T

type ToPrimitiveWithArray<T> = T extends any[]
  ? ToPrimitive<T[number]>
  : ToPrimitive<T>

type GetProps<P> = {
  [K in keyof P]: P[K] extends { type: infer F }
    ? ToPrimitiveWithArray<F>
    : {} extends P[K]
      ? any
      : ToPrimitiveWithArray<P[K]>
}

declare function VueBasicProps<
  P,
  D,
  C extends Record<string, any>,
  M
>(options: {
  props: P
  data: (this: GetProps<P>) => D
  computed: C & ThisType<D>
  methods: M & ThisType<D & GetComputed<C> & M & GetProps<P>>
}): any

虽然过程很艰难,但是我靠着不断尝试也算是写出来了。包括 T extends { (): infer R } 这种语法也是瞎试出来的。

下面是一些使用方法。

ts 复制代码
// 1. 函数类型(Function Type)
type F1 = () => R

// 2. 调用签名(Call Signature)------ 写在对象类型里
type F2 = { (): R }

// F1 和 F2 在大多数场景下完全等价
type IsEqual = F1 extends F2 ? true : false  // true

// 对象类型中可以同时有:调用签名 + 属性 + 方法
type MyFunc = {
  (): string           // 调用签名:直接调用返回 string
  (x: number): number  // 重载调用签名
  name: string         // 普通属性
  reset(): void        // 方法
}

declare const fn: MyFunc
fn()          // string
fn(42)        // number
fn.name       // string
fn.reset()    // void

// 构造签名(new 关键字)
type Newable = { new(): string }

// 推断构造函数返回类型
type InstanceType<T> = T extends { new(): infer R } ? R : never

然后看了官方的解法 #215 还是很优雅,是正向解法,我写的有点像逆向解法,写的才会那么乱。

ts 复制代码
/**
 * 推断 computed 对象的返回值类型
 * 将 computed 中每个函数的返回值类型提取出来
 * 例如: { count: () => number } → { count: number }
 */
type InferComputed<C extends Record<string, any>> = {
  [K in keyof C]: ReturnType<C[K]>
}

/**
 * Prop 的完整类型定义
 * 支持两种写法:
 *   1. 直接传构造函数/工厂函数:     props: { name: String }
 *   2. 传包含 type 字段的对象:      props: { name: { type: String } }
 */
type Prop<T = any> = PropType<T> | { type?: PropType<T> }

/**
 * PropType 支持单个构造器或构造器数组
 * 例如: String 或 [String, Number]
 */
type PropType<T> = PropConstructor<T> | PropConstructor<T>[]

/**
 * 构造器类型,支持两种形式:
 *   1. { new (...args: any[]): T & object }
 *      类的构造函数签名,T & object 确保:
 *        - new 只能返回对象类型(符合 JS 语义)
 *        - 与工厂函数分支互斥,避免推断歧义
 *        - 例如 String 构造函数:T & object = string & object = never,
 *          因此 String 不会匹配此分支,而是走工厂函数分支
 *
 *   2. { (): T }
 *      工厂函数签名(无参数),兜底处理原始类型
 *      例如 String 作为工厂函数调用时返回 string,T = string
 */
type PropConstructor<T = any> =
  | { new (...args: any[]): T & object }
  | { (): T }

/**
 * 从单个 Prop 定义中推断出其对应的 TS 类型
 *
 * 利用条件类型 + infer 提取泛型参数 T:
 *   - P extends Prop<infer T> 尝试从 P 中提取类型参数 T
 *   - 如果 P 是 {} 空对象,Prop<infer T> 中的 T 会被推断为 unknown
 *     此时返回 any,避免过于严格的类型约束
 *   - 否则直接返回推断出的 T
 *
 * 例如:
 *   InferPropType<{ type: String }>  → string
 *   InferPropType<{ type: Number }>  → number
 *   InferPropType<{}>                → any
 */
type InferPropType<P> = P extends Prop<infer T>
  ? unknown extends T
    ? any
    : T
  : any

/**
 * 推断整个 props 对象的类型
 * 遍历 props 定义对象,对每个 key 调用 InferPropType 提取类型
 *
 * 例如:
 *   InferProps<{ name: { type: StringConstructor }, age: { type: NumberConstructor } }>
 *   → { name: string, age: number }
 */
type InferProps<P extends Record<string, any>> = {
  [K in keyof P]: InferPropType<P[K]>
}

/**
 * 模拟 Vue 的 Options API 组件定义函数
 *
 * 泛型参数:
 *   @param P        - props 的原始定义类型
 *   @param D        - data 函数返回值类型
 *   @param C        - computed 对象类型(值为函数)
 *   @param M        - methods 对象类型
 *   @param Props    - 由 P 推断出的 props 实际类型(默认由 InferProps<P> 计算)
 *   @param Computed - 由 C 推断出的 computed 实际类型(默认由 InferComputed<C> 计算)
 *
 * options 参数:
 *   - props:    prop 定义对象
 *   - data:     返回组件数据,this 指向 Props(可访问 props)
 *   - computed: 计算属性对象,this 指向 Props & D & Computed & M(可访问所有上下文)
 *   - methods:  方法对象,this 指向 Props & D & Computed & M(可访问所有上下文)
 *
 * 返回值:
 *   Props & D & Computed & M 的交叉类型,即组件实例的完整类型
 */
declare function VueBasicProps<
  P extends Record<string, any>,
  D,
  C extends Record<string, any>,
  M,
  Props = InferProps<P>,
  Computed = InferComputed<C>
>(options: {
  props?: P
  // data 中 this 只能访问 props,此时 data/computed/methods 尚未初始化
  data(this: Props): D
  // computed 和 methods 的 this 可访问全部上下文(props + data + computed + methods)
  computed: C & ThisType<Props & D & Computed & M>
  methods: M & ThisType<Props & D & Computed & M>
}): Props & D & Computed & M

127. Hard - 223 - IsAny ※

判断一个类型是否是 any。这题解法很多的。T extends never ? true : false 其他类型只能得到 true 或者 false 只有 any 会得到 true | false

ts 复制代码
type IsAny<T> = boolean extends (T extends never ? true : false) ? true : false

在评论区还看到一个优雅的解法:

ts 复制代码
type IsAny<T> = 0 extends (1 & T) ? true : false;

这里利用了 any 的特殊规则,任何类型和 any 取交叉类型,还是 any

128. Hard - 270 - Typed Get

参考 lodash 的 get 函数,实现 TypeScript 的类型版本:给一个对象和 . 分隔的键,获取对应嵌套的值类型。

ts 复制代码
type Get<T, K extends string> = K extends keyof T
  ? T[K]
  : K extends `${infer L}.${infer R}`
    ? L extends keyof T
      ? Get<T[L], R>
      : never
    : never

129. Hard - 300 - String to Number

实现 Number.parseInt 的类型版本,把字符串转成数字。

ts 复制代码
type ToNumber<S extends string> = S extends `${infer N extends number}`
  ? N
  : never

130. Hard - 399 - Tuple Filter ※

实现一个类型 FilterOut<T, F>,用于从元组 T 中过滤掉指定类型 F 的元素。

ts 复制代码
type FilterOut<T extends any[], F> = 
T extends [infer L, ...infer R ]
  ? [L] extends [F]
    ? FilterOut<R, F>
    : [L, ...FilterOut<R, F>]
  : []

131. Hard - 472 - Tuple to Enum Object ※

在这个问题中,你实现的类型应当将给定的字符串元组转成一个行为类似枚举的对象。此外,枚举的属性一般是 pascal-case 的。如果传递了第二个泛型参数,且值为 true,那么返回值应当是一个 number 字面量。

ts 复制代码
type Enum<
  T extends readonly string[],
  N extends boolean = false,
  Index extends any[] = []
> = Readonly<
  Omit<
    T extends readonly [infer A extends string, ...infer B extends string[]]
      ? Record<Capitalize<A>, N extends true ? Index['length'] : A> &
          Enum<B, N, [...Index, 0]>
      : {},
    never
  >
>

这里有一点要注意,readonly 的数组也要 extends 后面也要加 readonly

ts 复制代码
type t1 = readonly ['a'] extends readonly [infer A, ...infer B] ? 1 : 2 // 1
type t2 = readonly ['a'] extends [infer A, ...infer B] ? 1 : 2 // 2

132. Hard - 545 - printf

根据 printf 解析字符串的规则,把字符串类型转成函数类型。

ts 复制代码
type Map = {
  d: number
  s: string
}
type Format<T extends string> = T extends `${string}%${infer F}${infer R}`
  ? F extends keyof Map
    ? (arg: Map[F]) => Format<R>
    : Format<R>
  : string

133. Hard - 553 - Deep object to unique

TypeScript 是结构类型系统,只要两个对象的字段和值类型一样,TS 就认为它们是同一种类型,可以互相赋值。

但有时候我们希望某个函数只接受特定的对象,而不是任何"长得像"的对象。

实现一个类型工具 DeepObjectToUniq<T>,对传入的对象类型做如下处理:

  1. 保留所有键名(字符串键、数字键都要保留)
  2. 保留所有属性的值类型不变
  3. 对对象本身以及所有深层嵌套的子对象,都打上唯一标记,让它们和原来的类型"不完全相同"
  4. 转换前后的类型可以互相赋值(不能破坏兼容性)
  5. 同一对象中,不同路径下结构相同的子对象,转换后必须是不同的类型

这道题目比较难理解,为了让对象和之前不同,但是还可以相互赋值,可以添加任意一个可选的属性。

ts 复制代码
type Foo = { foo: 2; baz: Quz; bar: Quz }
type Bar = { foo: 2; baz: Quz; bar: Quz & { quzz?: 0 } }

同一对象中,不同路径下结构相同的子对象转换后还必须不同,比如转换后,Foo[baz]Foo[bar] 不同,所以我们考虑把键也放在这个新增的可选属性中,保证唯一性。

然后不同对象,相同的属性也视为不同,比如转换后,Foo[baz]Bar[baz] 不同,所以把整个对象也放在新增的可选属性中,保证唯一性。

所以代码为:

ts 复制代码
type DeepObjectToUniq<O extends object, N = O> = Omit<
  {
    [K in keyof O]: O[K] extends object ? DeepObjectToUniq<O[K], N | K> : O[K]
  } & Partial<Record<symbol, N>>,
  never
>

134. Hard - 651 - Length of String 2

计算一个字符串的长度,字符串最长可能有999个字符。

ts 复制代码
type LengthOfString<S, Ans extends 0[] = []> =  S extends `${string}${string}${string}${string}${string}${string}${string}${string}${string}${string}${infer R}`
  ? LengthOfString<R, [...Ans, 0,0,0,0,0,0,0,0,0,0]>
  : S extends `${string}${infer R}`
    ? LengthOfString<R, [...Ans, 0]>
    : Ans['length']

135. Hard - 730 - Union to Tuple ※

把联合转成元组。这题真是想破脑袋也想不出来。

ts 复制代码
// https://github.com/type-challenges/type-challenges/issues/737
// 联合类型转交叉类型
type UnionToIntersection<T> = (T extends any ? (x: T) => any : never) extends (
  x: infer U
) => any
  ? U
  : never

// 取联合类型的最后一个元素:LastUnion<1 | 2 | 3> => 3
// 先构造函数类型的联合 (x: 1) => any | (x: 2) => any | (x: 3) => any
// 在转成交叉类型 ((x: 1) => any) & ((x: 2) => any) & ((x: 3) => any)
// 因为对重载函数做条件类型推断时,只使用最后一个重载
// 所以L为3
type LastUnion<T> = UnionToIntersection<
  T extends any ? (x: T) => any : never
> extends (x: infer L) => any
  ? L
  : never

// 联合类型转元组
type UnionToTuple<T, Last = LastUnion<T>> = [T] extends [never]
  ? []
  : [...UnionToTuple<Exclude<T, Last>>, Last]

UnionToIntersection 是前面的题目,不多说。

重点是 LastUnion 每次取元组最后一个元素:根据 TypeScript 2.8 的规范对重载函数做条件类型推断时,只使用最后一个重载

ts 复制代码
declare function foo(x: string): number;
declare function foo(x: number): string;
declare function foo(x: boolean): boolean;
type T30 = ReturnType<typeof foo>; // boolean

接下来每次取元组最后一个元素即可,直到元组为 never

136. Hard - 847 - String Join ※

实现一个连接字符串的函数的类型。

这个函数是返回一个函数,形如 () => () => xx,调用的时候是串行调用 join('-')('a', 'b', 'c')

一开始写成了上面那种错误的方式,这样写 P 读取不到第二个函数的入参类型,永远是 [],改成下面的方式就正确了。

ts 复制代码
// 错误
// declare function join<D extends string, P extends string[]>(delimiter: D):
//    (...parts: P) => JoinPartsWithDelimiter<D, P>

// 正确
declare function join<D extends string>(delimiter: D):
    <P extends string[]>(...parts: P) => JoinPartsWithDelimiter<D, P>

137. Hard - 956 - DeepPick

实现一个名为 DeepPick 的类型,它扩展了 Pick 实用类型。该类型接受两个参数。

ts 复制代码
type DeepPickSingle<O, P> = P extends `${infer K}.${infer R}`
  ? Record<K, DeepPickSingle<O[K & keyof O], R>>
  : P extends keyof O
      ? Record<P, O[P]>
      : unknown

type DeepPick<O, P> = (
  P extends any ? (args: DeepPickSingle<O, P>) => any : never
) extends (args: infer P) => any
  ? P
  : never

先实现一个支持单一属性的 DeepPick,可以根据属性路径 . 分割来读取嵌套类型。再根据前面学会的联合转交叉,把多个结果转成交叉类型。

138. Hard - 1290 - Pinia

实现 Pinia 库的类型版本。

参考之前实现 Vue 的知识点,这个没有特别难。

ts 复制代码
type getObjReturn<T> = {
  [K in keyof T]: T[K] extends () => infer R ? R : unknown
}

declare function defineStore<S, G, A>(store: {
  id: string
  state: () => S
  getters: G & ThisType<getObjReturn<G> & Readonly<S>>
  actions: A & ThisType<Readonly<getObjReturn<G>> & A & S>
}): Omit<S & getObjReturn<G> & A, never>

139. Hard - 1383 - Camelize

实现 Camelize 类型: 将对象属性名从蛇形命名snake_case(下划线命名) 转换为小驼峰命名camelCase。

ts 复制代码
type CamelizeString<S> = S extends `${infer A}_${infer B}`
  ? `${A}${CamelizeString<Capitalize<B>>}`
  : S
type Camelize<T> = T extends [infer F, ...infer R]
  ? [Camelize<F>, ...(Camelize<R> extends any[] ? Camelize<R> : [])]
  : T extends object
    ? {
        [K in keyof T as CamelizeString<K>]: Camelize<T[K]>
      }
    : T

140. Hard - 2059 - Drop String

删除字符串中的指定字符。

ts 复制代码
type GetStrUnion<S> = S extends `${infer A}${infer B}` ? A | GetStrUnion<B> : ''
type DropString<S, R, U = GetStrUnion<R>> =
  S extends `${infer A}${infer B}`
    ? A extends U ? DropString<B, R> : `${A}${DropString<B, R>}`
    : ''

141. Hard - 2822 - Split

实现 Split 的类型版本。

ts 复制代码
type Split<
  S extends string,
  SEP extends string = 'never'
> = S extends `${infer A}${SEP}${infer B}`
  ? [A, ...Split<B, SEP>]
  : S extends SEP
    ? []
    : string extends S
      ? string[]
      : [S]

142. Hard - 2828 - ClassPublicKeys ※

实现一个泛型 ClassPublicKeys<T>,用于获取一个类的所有公开(public)属性键名

ts 复制代码
type ClassPublicKeys<C> = keyof C

keyof 作用于类时,天然只返回 public 成员,因为 private/protected 成员在结构类型层面对外不可见。

143. Hard - 2857 - IsRequiredKey

实现泛型 IsRequiredKey<T, K> 返回 K 是否都是 T 中的 required 属性。

参考前面的 #57 Get Required 即可。

ts 复制代码
type IsRequiredKey<T, K extends keyof T> = false extends (
  K extends any ? (T[K] extends Required<T>[K] ? true : false) : never
)
  ? false
  : true

144. Hard - 2949 - ObjectFromEntries

实现类型版本的 Object.fromEntries

ts 复制代码
type ObjectFromEntries<T extends [string, any]> = {
  [K in T[0]]: T extends any ? T[0] extends K ? T[1] : never : never
}
// 更聪明一点
type ObjectFromEntries<T extends [string, any]> = {
  [K in T[0]]: T extends [K, any] ? T[1] : never
}

145. Hard - 4037 - IsPalindrome

实现类型 IsPalindrome<T> 检查一个字符串是否是回文串。

ts 复制代码
type Reverse<T extends string> = T extends `${infer L}${infer R}`
  ? `${Reverse<R>}${L}`
  : ''
type IsPalindrome<T extends string | number> = Reverse<`${T}`> extends `${T}`
  ? true
  : false

先把字符串逆转,再看逆转前后是否相等。

相关推荐
华仔啊2 小时前
除了防抖和节流,还有哪些 JS 性能优化手段?
前端·javascript·vue.js
CHU7290352 小时前
随时随地学新知——线上网课教学小程序前端功能详解
前端·小程序
清粥油条可乐炸鸡2 小时前
motion入门教程
前端·css·react.js
这是个栗子2 小时前
【Vue3项目】电商前台项目(四)
前端·vue.js·pinia·表单校验·面包屑导航
前端Hardy2 小时前
Electrobun 正式登场:仅 12MB,JS 桌面开发迎来轻量化新方案!
前端·javascript·electron
树上有只程序猿2 小时前
新世界的入场券,不再只发给程序员
前端·人工智能
confiself2 小时前
deer-flow前端分析
前端
刘宇琪2 小时前
Vite 生产环境代码分割与懒加载优化
前端
恋猫de小郭2 小时前
让你的 OpenClaw 带你学习,清华开源 AI 私人导师 OpenMAIC
前端·人工智能·ai编程