首页 > 解决方案 > 回调参数的条件类型

问题描述

这些是我的类型:

export type Mapping =
    | {
          type: 'ok';
          data: Record<string, string>;
      }
    | {
          type: 'error';
          data: Error;
      };

export type Payload<T> = Extract<Mapping, { type: T }>['data'];

export interface Callback<T> {
    (data: Payload<T>): void;
}

export type Store = {
    [K in Mapping['type']]: Set<Callback<K>>;
};

这是我的代码:

const storage: Store = {
    ok: new Set(),
    error: new Set(),
};

function store<T extends Mapping['type']>(
    type: T,
    callback: Callback<T>
): void {
    storage[type].add(callback);
}

我得到的错误:

Argument of type 'Callback<T>' is not assignable to parameter of type 'Callback<"ok"> & Callback<"error">'.
  Type 'Callback<T>' is not assignable to type 'Callback<"ok">'.
    Types of parameters 'data' and 'data' are incompatible.
      Type 'Record<string, string>' is not assignable to type 'Payload<T>'.
        Type 'Record<string, string>' is not assignable to type 'Extract<{ type: "error"; data: Error; }, { type: T; }>["data"]'.
          Type 'Record<string, string>' is missing the following properties from type 'Error': name, messagets(2345)

我想实现,如果某些回调存储在其中一个storage.okstorage.error可以处理正确的参数类型。所以我尝试使用条件类型,轻松地为回调参数类型添加更多选项。我认为错误是'Callback<"ok"> & Callback<"error">'因为它不应该是&。但是错误在哪里?具有正确的storage类型:

type Store = {
    error: Set<Callback<"error">>;
    ok: Set<Callback<"ok">>;
}

编辑:问题来自.add功能:

(method) Set<T>.add(value: Callback<"ok"> & Callback<"error">): Set<Callback<"ok">> | Set<Callback<"error">>

从哪里来&

标签: typescripttypes

解决方案


这是编译器未能执行必要的高阶类型分析来验证您在 的实现中所做的事情store()是安全的。


让我们定义PickStore<T>,如下所示:

type PickStore<T extends Mapping['type']> = { [K in T]: Set<Callback<K>> };

这本质上与Pick<Store, T>和 的超类型相同Store但编译器无法进行高阶类型分析来验证. 对于 的任何特定T,编译器可以看到它PickStore<T>是 的超类型Store

const sOk: PickStore<"ok"> = storage; // okay
const sError: PickStore<"error"> = storage; // okay
const sBoth: PickStore<"ok" | "error"> = storage; // okay
const sNeither: PickStore<never> = storage; // okay

但是,当您尝试对generic T执行相同操作时,编译器会犹豫:

function test<T extends Mapping['type']>() {
  const sT: PickStore<T> = storage; // error!
  // Type 'Store' is not assignable to type 'PickStore<T>'
}

这是你的问题。如果您可以扩大storage到函数PickStore<T>内部store,编译器将能够看到它storage[type]是 type Set<Callback<T>>,因此您可以毫无问题地add()使用callback参数。

相反,编译器只知道storage[type]可以分配给union Set<Callback<"ok">> | Set<Callback<"error">。这是真的,但对于您的目的来说不够具体。和之间的相关性丢失。(有关编译器无法跟踪多个值之间类型相关性的问题,请参阅microsoft/TypeScript#30581 。)因此,该方法被视为两种函数类型的联合。而且,当您调用函数类型的联合时,编译器需要其参数的交集,以保证类型安全。(如果你知道你有一个函数是-taker 或storage[type]callbackadd()XY-taker 但你不确定哪个,你最好给它一个X & Y安全的。)


我建议,在编译器无法验证某些高阶类型分析的这种情况下,您可以使用类型断言来告诉编译器您正在做的事情是安全的。这将保证安全的负担从编译器转移到了你身上,所以你应该小心。无论如何,它可能看起来像这样:

function store<T extends Mapping['type']>(
  type: T,
  callback: Callback<T>
): void {
  (storage as any as PickStore<T>)[type].add(callback);
}

在这里,我们刚刚对编译器进行了断言,storage可以将其视为 a (我们需要通过orPickStore<T>的中间断言,因为编译器确实存在关系问题)。过了那个障碍,线路的其余部分顺利通过。unknownany


Playground 代码链接


推荐阅读