☰
type-challenges 第 533 题 Concat 解析:在类型系统里实现 `Array.concat`
2026/10/1 23:39:39 网站建设 项目流程
  • 示例工程

【免费下载链接】type-challenges

Collection of TypeScript type challenges with online judge

项目地址:https://gitcode.com/GitHub_Trending/ty/type-challenges
点击查看免费下载

导读

Concat 是 type-challenges 题库中编号 533 的 easy 级数组(array)主题挑战,由 Andrey Krasovsky(GitHub 账号 @bre30kra69cs 中(difficulty: easy,tags: array)。本题要求把 JavaScript 内置的Array.concat方法移植到 TypeScript 的类型系统中:给定两个数组/元组类型参数,返回一个按「从左到右」顺序合并两者元素的新数组类型。读完本文,你将掌握变长元组类型(variadic tuple types)与 spread 语法在类型层的核心用法,并能够完整通过仓库自带的 5 组正向用例与 1 组反向用例。

题目要求:在类型系统中实现Array.concat

需求描述

原题(README.ja.md)的表述是:在类型系统中实现 JavaScript 的Array.concat函数。这个类型接收两个参数,返回一个按照输入参数顺序(ltr order,从左到右)包含全部元素的新数组。

与运行时[1, 2].concat([3, 4]) // => [1, 2, 3, 4]的行为对应,类型层面Concat<T, U>的目标是把两个数组/元组类型拼接成一个新元组类型:

type Result = Concat<[1], [2]>; // expected to be [1, 2]

起点模板

仓库中供解题者填写的起点文件是 template.ts,只有一个占位实现:

type Concat<T, U> = any

任务就是把any替换为真正的类型计算逻辑,同时保持Concat接收两个泛型参数的形式不变。

测试用例:5 组正向 + 1 组反向

解题是否通过,由 test-cases.ts 决定。它从@type-challenges/utils导入了Equal与Expect(该工具包源码位于 utils/index.d.ts,其中Equal基于著名的函数逆变比较技巧实现类型相等判定),并声明了一个as const元组:

import type { Equal, Expect } from '@type-challenges/utils' const tuple = [1] as const type cases = [ Expect<Equal<Concat<[], []>, []>>, Expect<Equal<Concat<[], [1]>, [1]>>, Expect<Equal<Concat<typeof tuple, typeof tuple>, [1, 1]>>, Expect<Equal<Concat<[1, 2], [3, 4]>, [1, 2, 3, 4]>>, Expect<Equal<Concat<['1', 2, '3'], [false, boolean, '4']>, ['1', 2, '3', false, boolean, '4']>>, ] // @ts-expect-error type error = Concat<null, undefined>

逐条拆解这些用例,可以提炼出实现必须满足的约束:

用例输入期望输出考察点
1Concat<[], []>[]两个空元组拼接仍是空元组
2Concat<[], [1]>[1]空元组 + 单元素元组
3Concat<typeof tuple, typeof tuple>[1, 1]readonly元组参与拼接
4Concat<[1, 2], [3, 4]>[1, 2, 3, 4]多元素元组按 ltr 顺序合并
5Concat<['1', 2, '3'], [false, boolean, '4']>['1', 2, '3', false, boolean, '4']混合字面量类型、原始类型与联合类型
反向Concat<null, undefined>编译报错(@ts-expect-error)参数必须是数组/元组,而非null、undefined

注意用例 3:const tuple = [1] as const的推断类型是readonly [1],因此实现必须能接受readonly修饰的元组;而反向用例要求Concat<null, undefined>触发类型错误,说明两个泛型参数上应存在数组/元组的约束(extends限制),这正是「约束参数」与「返回新类型」的双重考点。

参考实现:一行 spread 解决问题

结合用例约束,规范且能全部通过的实现如下:

type Concat<T extends readonly unknown[], U extends readonly unknown[]> = [...T, ...U]

要点拆解:

  1. T extends readonly unknown[]/U extends readonly unknown[]:对两个参数施加元组约束。之所以写readonly unknown[]而不是unknown[],是因为用例 3 传入的是readonly [1];若约束写成可变数组类型,readonly元组无法满足约束而报错。
  2. [...T, ...U]:利用 TS 3.0 引入的变长元组类型(variadic tuple types),把T和U的已知元素按 spread 顺序展开进新元组,天然保证 ltr 顺序。
  3. 返回新数组:spread 每次都会产生全新的元组类型,绝不修改输入,这与Array.concat返回新数组(而非原地修改)的语义一致。

若把约束放宽为只要求数组而不带readonly,则用例 3 无法通过;若不写约束直接type Concat<T, U> = [...T, ...U],则反向用例Concat<null, undefined>不会报错。所以约束这一行不是装饰,而是通过测试的硬性前提。

底层原理:变长元组类型与 spread

为什么能在类型里「展开」元组

TypeScript 3.0 引入的变长元组类型允许在元组类型的任意位置使用rest 元素。[...T, ...U]中的 spread 会在编译期被求值:当T、U都是具体元组时,编译器会计算出展开后的完整元素序列,得到一个可被Equal精确比较的新元组类型。

与readonly的关系

readonly [1]表示只读元组。在普通函数约束(T extends unknown[])下,只读数组不被视为可赋值给可变数组,因此必须显式声明extends readonly unknown[]。这也是为什么测试里特意使用const tuple = [1] as const来构造readonly [1]——本题把「只读元组也能拼接」作为一项显式能力要求。

空元组的处理

当T = []或U = []时,spread 空元组不产生任何元素,[...[], ...[1]]自然收敛为[1],因此用例 1、2 无需任何特判分支。

同族挑战:Push 与 Unshift

info.yml 的related字段标明了本题的两个关联挑战:3057 Push 与 3060 Unshift。三个挑战共用同一条「spread 构建新元组」的心法,只是插入位置不同:

  • Push(questions/03057-easy-push):把单个元素追加到末尾,模板为 template.ts,实现为type Push<T, U> = [...T, U]。其 test-cases.ts 还额外用// @ts-expect-error验证了Push<number[], string>等错误用法必须报错,说明它同样依赖参数约束。
  • Unshift(questions/03060-easy-unshift):把单个元素插入头部,实现为type Unshift<T, U> = [U, ...T],test-cases.ts 验证了Unshift<['1', 2, '3'], boolean>得到[boolean, '1', 2, '3']。

三者对比可归纳出规律:「拼接」类类型操作的本质,就是在结果元组的合适位置放置 spread(...)。Concat 是其中唯一需要展开两个元组参数的变体,把它吃透后,Push/Unshift 几乎可以「顺手解出」。仓库根目录的 README.zh-CN.md 与 TODOs.md 记录了完整的挑战列表与推进规划,方便按难度递进刷题。

本地验证方式

仓库使用 pnpm 管理依赖(见根目录 package.json 中的packageManager: pnpm@8.12.1),并提供了@type-challenges/utils工作区包(utils/package.json 显示其版本为 0.1.1,类型定义见 utils/index.d.ts)。验证某题是否通过,可在安装依赖后对对应目录执行 TypeScript 类型检查,例如:

pnpm install pnpm -C questions/00533-easy-concat exec tsc --noEmit test-cases.ts

在支持 TypeScript 的编辑器(如 VSCode)中直接打开 test-cases.ts,观察Expect行是否出现类型错误,是更直观的即时反馈方式。当模板 template.ts 中的any被正确替换后,5 组Expect<Equal<...>>全部成立,Concat<null, undefined>处的@ts-expect-error也正好被「按预期报错」消费,测试即全部通过。

常见误区与排查思路

  1. 约束漏写readonly:T extends unknown[]会导致Concat<typeof tuple, typeof tuple>直接报「readonly 元组不可赋值给可变数组」,用例 3 失败。
  2. 约束不写:反向用例Concat<null, undefined>不会产生类型错误,@ts-expect-error反而会因「未发生预期错误」而报错。
  3. 顺序写反:写成[...U, ...T]会通过类型检查但输出顺序颠倒,无法满足 ltr 语义,用例 4、5 失败。
  4. 误用concat之类运行时方法:本题完全在类型层求解,模板文件中不应出现任何 JS 运行时表达式;Equal做的是结构相等比较,只有类型层面真正产出[1, 2, 3, 4]这样的元组才能通过。

综上,Concat 虽被标记为 easy,却是一次性覆盖了「泛型约束」「readonly 兼容」「变长元组 spread」「正向/反向用例验证」多个基础能力的优质入门题,也是后续大量数组类 hard 挑战(如 Chunk、Flatten 等)的通用地基。

  • 示例工程

【免费下载链接】type-challenges

Collection of TypeScript type challenges with online judge

项目地址:https://gitcode.com/GitHub_Trending/ty/type-challenges
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询