【问题标题】:How to handle a reduce on union type如何处理联合类型的减少
【发布时间】:2022-11-30 00:04:54
【问题描述】:

我有以下代码:(TS Playground link)

type TDeliveriesStatsDatum<TName = string> = {name: TName; value: number};
type TDeliveriesStatsData<TName = string> = TDeliveriesStatsDatum<TName>[];

interface IDeliveriesStats {
    meta: {
        delivery_count: number;
        transport_count: number;
    };
    progress: TDeliveriesStatsData<"done" | "not_done" | "cancelled">;
    disputes: TDeliveriesStatsData<"disputed" | "undisputed">;
    loading_punctuality: TDeliveriesStatsData<"ontime" | "delayed" | "unknown">;
    unloading_punctuality: TDeliveriesStatsData<"ontime" | "delayed" | "unknown">;
    cmr_document: TDeliveriesStatsData<"done_with_cmr_document" | "done_without_cmr_document">;
    non_cmr_document: TDeliveriesStatsData<
        "done_with_non_cmr_document" | "done_without_non_cmr_document"
    >;
    // for delays and durations, name is a string containing the values in minutes of the steps
    // ex: "_15",  "15_30" and "30_" for "less than 15 minutes", "between 15 and 30 minutes" and "more than 30 minutes"
    loading_delay: TDeliveriesStatsData;
    unloading_delay: TDeliveriesStatsData;
    loading_duration: TDeliveriesStatsData;
    unloading_duration: TDeliveriesStatsData;
}

type DeliveriesStatsKeys = "progress" | "disputes" | "cmr_document" | "non_cmr_document";


type TPieChartData<T extends DeliveriesStatsKeys> = {
    augmentedData: {name: string, value: number, dataKey: string, fill: string}[] 
} & {
    [K in IDeliveriesStats[T][0]["name"]]: number;
};

export const formatPieChartData = <K extends DeliveriesStatsKeys>(
    data: IDeliveriesStats[K]
): TPieChartData<K> => {
    return data.reduce(
        (acc: TPieChartData<K>, datum: IDeliveriesStats[K][0]) => {
            // keep these values to extract them for KPIs
            acc[datum.name] = datum.value;

            acc.augmentedData.push({
                ...datum,
                dataKey: datum.name,
                fill: "colorsAndLabelsByDataKey[datum.name].fill," // written this way to avoid adding too much unrelated code
            });
            return acc;
        },
        {augmentedData: []} as TPieChartData<K>
    );
};

编译器错误如下:

This expression is not callable.
  Each member of the union type '{ (callbackfn: (previousValue: TDeliveriesStatsDatum<"done" | "not_done" | "cancelled">, 
currentValue: TDeliveriesStatsDatum<"done" | "not_done" | "cancelled">, 
currentIndex: number, array: TDeliveriesStatsDatum<...>[]) => TDeliveriesStatsDatum<...>): TDeliveriesStatsDatum<...>;
 (callbackfn: (previousValue: TDeliver...' 
has signatures, but none of those signatures are compatible with each other.

我如何输入 IDeliveriesStatsTPieChartData 让 Typescript 理解 acc[datum.name] 是正确输入的?

【问题讨论】:

  • 这是 TS 不直接支持的另一个影响相关联盟ms/TS#30581 中所述,推荐的修复是 ms/TS#47109 中描述的某种重构。不仅如此,这里的示例的最终代码看起来像 this playground link。如果这完全解决了您的问题,我可以写一个完整的答案。否则,我错过了什么?
  • @jcalz 谢谢,这似乎是一个很好的解决方案。我不确定我是否完全理解你是如何想到它的;)另外我认为重新定义 _acc 来转换似乎有点奇怪?我会这样写:(acc as Record&lt;DeliveriesStatsName[K], number&gt;)[datum.name] = datum.value;
  • 通过x as T 的类型断言允许扩大(通常是安全的)和缩小(通常是不安全的)。变量上的类型注释(通过const _x: T = x)只允许加宽。因此,如果您正在做一些安全的事情并希望编译器对其进行验证,则注释比断言更可取。如果您只是在寻找易用性,那么类型断言就可以了,但是您并没有真正让 TypeScript 按照要求“理解 acc[datum.name] 是正确键入的”。当我写下答案时,我会解释更多。
  • 谢谢,我不明白为什么我们首先需要扩大类型;为什么 TS 不理解直接将 datum.name 分配给 acc? (它失败了 Type 'number' is not assignable to type 'PieChartData&lt;K&gt;[DeliveriesStatsName[K]]'.
  • 我不知道它为什么失败的权威来源;通常,编译器在使用(通用)类型K 的键索引到Record&lt;K, V&gt; &amp; Foo 时会感到困惑。加宽本质上是解决这个问题的一种方法。

标签: typescript


【解决方案1】:

这里的一个问题是编译器无法遵循acc 类型和datum.name 类型之间的相关性,除非这些类型以特定方式表示,如microsoft/TypeScript#47109 中所述。否则相关性将丢失,并且类型检查将恢复为将 accdatum.name 视为约束为 unions 的不相关类型,并且它抱怨源属性和目标属性对应于不同键的可能性(如在microsoft/TypeScript#30769)。当然有没有这种可能性(或者无论如何不太可能),但编译器看不到它。

另一个问题是编译器将 data.reduce() 调用视为尝试调用泛型方法的联合。那是因为 data 是一个泛型类型,如果急切求值,它只能被视为一个数组,并且将 K 扩大到它的约束。不真正支持调用泛型方法的联合;见microsoft/TypeScript#44373。同样,data 的类型需要以特定方式表示,以便编译器一开始就知道这是一个具有单个泛型 reduce() 方法的单一数组类型。


所需的重构是根据 generic 键类型将所有内容写入 indexmapped type... 通常作为基本“映射”类型的函数。您可以这样做:

// basic mapping type
interface DeliveriesStatsName {
    progress: "done" | "not_done" | "cancelled"
    disputes: "disputed" | "undisputed"
    cmr_document: "done_with_cmr_document" | "done_without_cmr_document"
    non_cmr_document: "done_with_non_cmr_document" | "done_without_non_cmr_document"
}

type DeliveriesStats<K extends keyof DeliveriesStatsName> =
    DeliveriesStatsData<DeliveriesStatsName[K]>;

interface AugmentedData {
    augmentedData: {
        name: string, value: number, dataKey: string, fill: string;
    }[]
}

type BasePieChartData<K extends keyof DeliveriesStatsName> =
    Record<DeliveriesStatsName[K], number>;

type PieChartData<K extends keyof DeliveriesStatsName> =
    BasePieChartData<K> & AugmentedData;

在上面,DeliveriesStats&lt;K&gt; 与您的 IDeliveriesStats[K] 一样,但它是根据映射类型 DeliveriesStatsName 明确表示的。类似地,除了显式映射类型表示之外,PieChartData&lt;K&gt; 与您的 TPieChartData&lt;K&gt; 一样。

目标是编译器应该看到 data 是单一数组类型,并且 acc[datum.name] = datum.value 正在使用 XXX 类型的键索引到 Record&lt;XXX, number&gt;,因此接受 number

我们几乎实现了这些目标,但还不完全是:

export const formatPieChartData = <K extends keyof DeliveriesStatsName>(
    data: DeliveriesStats<K>
): PieChartData<K> => {
    return data.reduce((acc, datum) => {

        acc[datum.name] = datum.value; // error!
        // Type 'number' is not assignable to type 'PieChartData<K>[DeliveriesStatsName[K]]'.

        acc.augmentedData.push({
            ...datum,
            dataKey: datum.name,
            fill: "colorsAndLabelsByDataKey[datum.name].fill,"
        });
        return acc;
    }, { augmentedData: [] } as AugmentedData as PieChartData<K>);
    // double assertion needed here to get the compiler to see the types as related

};

data.reduce() 可调用的目标已经实现,这很好。但是acc[datum.name] = datum.value 仍然导致错误。这是因为通用记录类型的交叉问题。我不知道关于这个的权威问题(我能找到的最接近的是ms/TS#38796),但是如果你有一个像Record&lt;K, V&gt; &amp; Foo这样的类型,其中K是通用的,并尝试使用键@对其进行索引987654356@,编译器丢失线程。这里的解决方法是安全地将 Record&lt;K, V&gt; &amp; Foo 扩展为 Record&lt;K, V&gt;,然后对其进行索引。有关详细信息,请参阅this answer

所以让我们这样做:

export const formatPieChartData = <K extends keyof DeliveriesStatsName>(
    data: DeliveriesStats<K>
): PieChartData<K> => {
    return data.reduce((acc, datum) => {

        const _acc: BasePieChartData<K> = acc; // okay
        _acc[datum.name] = datum.value; // okay

        acc.augmentedData.push({
            ...datum,
            dataKey: datum.name,
            fill: "colorsAndLabelsByDataKey[datum.name].fill,"
        });
        return acc;
    }, { augmentedData: [] } as AugmentedData as PieChartData<K>);

};

acc 扩展到_acc 被编译器验证为安全,然后对_acc[datum.name] 的分配按预期工作!

Playground link to code

【讨论】:

    猜你喜欢
    • 2020-06-30
    • 2022-10-02
    • 2018-05-16
    • 2020-02-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-08-27
    • 2022-08-18
    相关资源
    最近更新 更多