You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

TypeScript:使用UnionToIntersection时保留子表达式的问题

TypeScript可空类型解析为never的问题解决方法

问题背景

以下是简化自CSV写入库的代码,核心问题是SpecToValue<['nullable', 'string']>被解析为never,而非预期的string | null,导致传入合法值时触发错误:

type UnionToIntersection<U> =
    (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

type Spec = 'string' | 'timestamp' | ['nullable', Spec];

type SpecToValue<T> = UnionToIntersection<
    T extends 'string' ? string :
    T extends 'timestamp' ? Date :
    T extends ['nullable', infer InnerT] ? SpecToValue<InnerT> | null :
    never
>;

declare function writeValue<T extends Spec>(spec: T, value: SpecToValue<T>): void;

function test(s1: 'string', s2: 'string' | 'timestamp', s3: ['nullable', 'string']) {
    writeValue(s1, 'hello');
    writeValue(s1, false); // error (正确)
    writeValue(s1, null);  // error (正确)

    writeValue(s2, 'hello'); // error (正确)
    writeValue(s2, false);   // error (正确)
    writeValue(s2, null);    // error (正确)

    // 问题:此处期望允许传入'string'或null,但实际报错
    writeValue(s3, 'hello'); // ERROR (错误)
    writeValue(s3, false);   // error (正确)
    writeValue(s3, null);    // ERROR (错误)
}

问题根源在于UnionToIntersection会将联合类型转换为交叉类型:当处理可空分支时,SpecToValue<InnerT> | null会被转换成string & null,最终结果为never。

补充定义(实际代码结构)

实际代码中将简单类型与可空类型分离:

type SimpleSpec = 'string' | 'timestamp';
type Spec = SimpleSpec | ['nullable', SimpleSpec];

type SimpleSpecToValue<T> =
    T extends 'string' ? string :
    T extends 'timestamp' ? Date :
    never;
type SpecToValue<T> =
    T extends SimpleSpec ? SimpleSpecToValue<T> :
    T extends ['nullable', infer InnerT] ? (SpecToValue<InnerT> | null) : null

解决方案

调整类型逻辑,仅对简单类型的联合应用UnionToIntersection,保留可空类型的联合特性:

type UnionToIntersection<U> =
    (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

type SimpleSpec = 'string' | 'timestamp';
type Spec = SimpleSpec | ['nullable', SimpleSpec];

type SimpleSpecToValue<T> =
    T extends 'string' ? string :
    T extends 'timestamp' ? Date :
    never;

// 仅针对SimpleSpec的联合类型应用交叉转换,确保联合类型的Spec返回never
type ResolveSimpleSpec<T extends SimpleSpec> = UnionToIntersection<SimpleSpecToValue<T>>;

type SpecToValue<T> =
    T extends SimpleSpec ? ResolveSimpleSpec<T> :
    // 处理可空类型时直接返回联合,不应用交叉转换
    T extends ['nullable', infer InnerT] ? InnerT extends SimpleSpec ? (ResolveSimpleSpec<InnerT> | null) : never :
    never;

declare function writeValue<T extends Spec>(spec: T, value: SpecToValue<T>): void;

function test(s1: 'string', s2: 'string' | 'timestamp', s3: ['nullable', 'string']) {
    writeValue(s1, 'hello'); // 正常
    writeValue(s1, false); // error (正确)
    writeValue(s1, null);  // error (正确)

    writeValue(s2, 'hello'); // error (正确,联合类型无合法值)
    writeValue(s2, false);   // error (正确)
    writeValue(s2, null);    // error (正确)

    writeValue(s3, 'hello'); // 正常 (符合预期)
    writeValue(s3, false);   // error (正确)
    writeValue(s3, null);    // 正常 (符合预期)
}

逻辑说明

  1. ResolveSimpleSpec:仅对SimpleSpec类型应用UnionToIntersection,当传入'string' | 'timestamp'这类联合类型时,会得到string & Date即never,确保无法传入任何合法值。
  2. 可空类型处理:直接返回ResolveSimpleSpec<InnerT> | null的联合类型,避免交叉转换破坏可空特性。

内容的提问来源于stack exchange,提问作者Kannan Goundan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 08:24:51