不要求“一模一样”,但要看 TS 怎么推导 T:
1)数组元素都同类型:最简单
head([1, 2, 3]) // T = number
head(['a', 'b']) // T = string
2)数组元素不同类型:也可以,TS 会把 T 推成“联合类型”
head([1, 'a', true]) // T = number | string | boolean
// 返回类型就是 (number | string | boolean) | undefined
3)如果你想“限制必须同类型”
那就不要让它推成联合:通常通过 你自己声明数组类型 或约束来实现,例如:
const nums: number[] = [1, 2, 3] // 强制元素都是 number
如果你写:
const nums: number[] = [1, 'a'] // ❌ 报错
工具方法本身写成泛型时,TS 允许混合数组,是因为它会把 T 推成联合类型(例如 T = number | string)。如果你想“工具方法直接拒绝混合数组”,有两种常用做法:
做法 A(最实用):让调用方给出“单一数组类型”
工具方法正常写:
function head<T>(list: T[]): T | undefined {
return list[0]
}
调用时通过变量类型或显式泛型来强制同类型:
const nums: number[] = [1, 2, 3]
head(nums) // ✅ T = number
head<number>([1, 2, 3]) // ✅
head<number>([1, 'a']) // ❌ 直接报错
做法 B(更严格):工具方法层面禁止 T 是联合类型(禁止混合)
type IsUnion<T, U = T> =
T extends any ? ([U] extends [T] ? false : true) : never
type NoUnion<T> = IsUnion<T> extends true ? never : T
function headStrict<T>(list: NoUnion<T>[]): NoUnion<T> | undefined {
return list[0]
}
headStrict([1, 2, 3]) // ✅
headStrict(['a', 'b']) // ✅
headStrict([1, 'a']) // ❌(T 会被推成 number | string,被禁止)
一般业务里推荐 做法 A(更直观、团队更好维护);做库/想强约束再用 做法 B。
总结
- 可以不一样:TS 会推导出联合类型 T = A | B | ...
- 想必须一样:就把数组声明为 number[] / string[] 这种单一元素类型数组
额外:有联合类型,就有阻止联合。
联合类型一般是 TS “没法确定只有一种类型”时自动推出来的(比如 [1, 'a'] → T = number | string)。要“阻止联合”,常用有两类办法:
1)在调用处阻止(最常用):显式指定“必须是单一类型”
- 给数组标注类型(推荐)
const nums: number[] = [1, 2, 3]
// const nums: number[] = [1, 'a'] // ❌ 直接报错
head(nums) // T = number
- 调用时显式指定泛型
head<number>([1, 2, 3]) // ✅
head<number>([1, 'a']) // ❌
2)在工具函数层面强制禁止(更严格):让 T 不能是联合
type IsUnion<T, U = T> =
T extends any ? ([U] extends [T] ? false : true) : never
type NoUnion<T> = IsUnion<T> extends true ? never : T
function headStrict<T>(list: NoUnion<T>[]): NoUnion<T> | undefined {
return list[0]
}
headStrict([1, 2, 3]) // ✅
headStrict([1, 'a']) // ❌(推导出 T=number|string,被禁止)、
它的目标是:如果 TS 推导出 T 是联合类型(比如 number | string),就让它变成 never,从而在调用处报错,达到“禁止混合数组”的效果。
通常业务里用 第 1 种就够了;做通用库、想强约束再用 第 2 种。
思路: “阻止联合”的本质逻辑就是一句话:让 TS 在推导时就能确定:只能有一种类型(单一 T),不能出现第二种可能。
怎么做到“只能一种类型”(两条路)
-
路 1:在输入处就限定范围(最常用)
- 给变量/参数明确类型:
number[]、string[] - 或者调用时手动指定泛型:
head<number>(...) - 逻辑:你提前把“类型集合”定死了,TS 就不需要用 A | B 来兜底。
- 给变量/参数明确类型:
-
路 2:在工具函数签名里写规则,直接拒绝联合(更严格)
- 用类型体操判断 T 是否为联合
- 如果是联合就变 never 让调用报错
- 逻辑:允许 TS 推导出联合,但你声明“联合不合法”,让它在类型检查阶段失败。