缩小输入类型却致输出类型变宽?TypeScript泛型疑问
1. 为什么mapB的返回类型变成了{} | undefined?
这个问题出在TypeScript的泛型推导逻辑上。我们先看mapB的定义:
function mapB<T, U>(source: T, selector: (v: NonNullable<T>) => NonNullable<U>) { return (source == null || source === undefined) ? undefined : selector(source!); }
当你调用mapB(2, i => i * i)时,TypeScript需要推导泛型参数U。这里的关键是:selector的返回值类型被约束为NonNullable<U>,而i * i的类型是number。
在TypeScript中,所有非null/undefined的类型都是{}的子类型({}在这里代表“除了null和undefined之外的任何值”)。TypeScript的类型推导会倾向于选择最宽泛的符合条件的类型,所以它会把U推导为{}——因为NonNullable<{}>就是{}本身,而number确实是{}的子类型,完全满足selector的返回类型约束。
这样一来,mapB的返回类型就变成了undefined | NonNullable<U>,也就是undefined | {},这就是你看到的不符合预期的结果。
要解决这个问题,我们可以调整泛型参数的约束方式,直接让U代表selector的返回类型,同时约束它为非空类型:
function mapB<T, U extends NonNullable<unknown>>(source: T, selector: (v: NonNullable<T>) => U) { return (source == null || source === undefined) ? undefined : selector(source!); } const mapBResult = mapB(2, i => i * i); // 现在类型是number | undefined,符合预期
这样TypeScript会正确推导U为number,返回类型也就正常了。
2. 为什么需要添加非空断言source!?
这是TypeScript泛型控制流分析的局限性导致的。虽然我们在代码里写了source == null的判断,理论上else分支里的source肯定不是null或undefined,但因为source的类型是泛型T,TypeScript无法精确跟踪泛型类型在控制流中的缩小。
举个例子,如果T是一个联合类型,比如number | null | string,当source == null为false时,source的类型应该是number | string,也就是NonNullable<T>。但TypeScript的泛型系统目前没办法自动将source的类型从T缩小到NonNullable<T>——它无法确定T的具体结构,所以没办法在控制流中完成这个类型缩小。
因此,我们需要手动添加source!非空断言,告诉编译器:“我保证这里的source一定不是null或undefined”,这样才能通过类型检查。
内容的提问来源于stack exchange,提问作者JustinM

