先记住这个答案
泛型参数可通过 T = Default 设置默认类型。调用没有显式指定该参数、也没有其他足够的推断选择时,默认类型可以成为结果;如果实参已经推断出数字,默认 string 不会强行覆盖它。类型参数的默认值必须满足自身约束,必需类型参数不能放在已有默认参数之后。默认类型只影响静态契约,不等于函数参数默认值,更不会在运行时自动创建一个字符串或实例。调整公开泛型的默认类型会改变省略类型参数的调用者,应该作为接口行为变化审查。
- 可用实参推断不会被默认类型机械覆盖
- 默认类型必须满足约束并遵守参数顺序
- 类型默认不负责运行时数据初始化
默认类型不是每次调用的固定类型
一个通用列表工厂没有输入时可以默认面向字符串,有数字输入时仍应产生数字列表。若默认类型总覆盖推断,泛型将无法适应实际输入,也会让合法数字调用被错误限制。应把默认理解为签名缺省选择的一部分。
示例在运行时明确处理 undefined,返回空数组或单元素数组。没有输入时类型默认是字符串数组,传入数字时由实参确定数字数组,显式选择布尔类型也可以返回空数组。空数组来自函数体分支,而不是类型系统偷偷创建了某个默认元素。
function list<T = string>(value?: T): T[] {
return value === undefined ? [] : [value];
}
const defaulted = list();
const inferred = list(7);
const chosen = list<boolean>();
// @ts-expect-error 显式选择 boolean 后不能传入数字
list<boolean>(7);三个结果分别是 string[]、number[] 和 boolean[],其中没有实参的两个调用在运行时都返回空数组。显式类型参数仍约束实参,默认机制不会把错误值转换成合法值。
参数顺序与约束需要一起设计
有默认类型的参数可以在类型实例化时省略,所以后面不应再出现必须手写的类型参数。默认值若不满足 extends 约束,声明本身就不成立。设计多个参数时,应把最常需要明确选择的放在前面,并避免依赖难理解的默认链。
后面的默认类型可以引用前面的参数,例如某个容器的元素列表默认依赖已选元素类型。这样的关系有助于减少重复书写,但默认并不总是继续推断的同义词。显式提供前面参数、遗漏后面参数时,应查看后者是否直接采用默认契约。
公开默认值变更为什么影响使用者
库把默认类型从 unknown 改成 any,可能让旧调用突然跳过检查;改成某个窄业务类型,又可能让之前能编译的代码报错。即使运行时实现完全没变,消费端看到的接口仍然变化,版本评审应包含省略参数的典型使用方式。
如果函数承诺返回具体 T 值,增加默认类型也不能解决实现无法构造任意 T 的问题。调用者仍可显式选择另一种合法类型,因此实现必须对所有允许选择成立。需要实际创建对象时,应接收工厂、解析器或明确返回基础结构。
容易答错的地方
- 没有显式写类型参数就一定用默认类型
- 实参和上下文可能已经提供更具体的推断结果,默认不会无条件覆盖它们;应查看实际调用点,而不是只看签名中的等号。
- T 默认是 string 就会在缺参时生成空字符串
- 类型注解在运行时被擦除,缺参时返回什么完全由函数体决定;需要默认值时必须写出对应的实际初始化逻辑。
面试官还会怎么问?
泛型默认值与可选函数参数可以独立存在吗?
可以,它们处理类型参数和运行时参数两个层面;给 T 默认类型并不意味着 value 可以省略,反过来可选 value 也不必有泛型默认值。
默认类型可以引用前面的类型参数吗?
可以在合法声明中建立这种依赖,例如默认集合类型使用前面的元素类型;仍要满足约束,并让调用者清楚省略后实际采用什么。
为什么默认 unknown 往往比默认 any 更严格?
unknown 会保留使用前检查的责任,而 any 容易让后续操作跳过检查;选择应体现真实接口承诺,而不只是为了让旧调用全部通过。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。