TypeScript区分联合过滤对区分键失败

时间:2019-06-02 06:58:15

标签: typescript discriminated-union mapped-types

我有以下用于跟踪异步请求状态的代码。它使用_typestatus作为鉴别符。

在以下代码中,我定义了两种AsyncStatus类型:LoginAsyncStatusSearchAsyncStatus。它们的区别在于_typesuccess value

问题在于TypeScript似乎错误地缩小了所区分联合的类型。

export type AsyncStatus<BrandT extends string, T = undefined> =
  | { id: string; _type: BrandT; error?: never; state: "loading"; value?: never; }
  | { id: string; _type: BrandT; error: Error; state: "error"; value?: never }
  | { id: string; _type: BrandT; error?: never; state: "success"; value: T };

export type ExtractAsyncStatusByType<
  TName extends ApiAsyncStatus["_type"],
  TType
> = TType extends AsyncStatus<TName, any> ? TType : never;

export type LoginAsyncStatus = AsyncStatus<"LOGIN", { refreshToken: string }>;
export type SearchAsyncStatus = AsyncStatus<"SEARCH", string[]>;
export type ApiAsyncStatus = LoginAsyncStatus | SearchAsyncStatus;

export type Registry = Partial<Record<ApiAsyncStatus["id"], ApiAsyncStatus>>;

export const getApiAsyncStatus = <T extends ApiAsyncStatus["_type"]>(
  registry: Registry,
  id: string,
  type: T,
): ExtractAsyncStatusByType<T, ApiAsyncStatus> | undefined => {
  let status = registry[id];
  if (status !== undefined && status._type !== type) {
    /**
     * Property 'value' is missing in type 
     *   '{ _type: T; error: Error; id: string; state: "error"; }'
     * but required in type 
     *   '{ id: string; _type: "SEARCH"; error?: undefined; state: "success"; value: string[]; }'
     * .ts(2322)
     */
    status = {
      _type: type,
      error: new Error(`Expected _type ${type}, but received ${status._type}`),
      id,
      state: "error",
    }; // err
  }
  return status as ExtractAsyncStatusByType<T, ApiAsyncStatus> | undefined;
};

我已经更新了最初的问题,其中的问题是在我不尝试动态创建状态的情况下返回适当的类型。

1 个答案:

答案 0 :(得分:0)

我将重复我的评论并从那里继续:

区别联合仅在您希望使用具体类型时才真正起作用,而不是泛型。此外,在infer_datetime_format的实现中,类型getApiAsyncStatus()是一个无法解析的泛型参数,并且编译器在尝试验证值是否可以分配给依赖于此条件类型的条件类型方面没有做太多工作未解决的泛型。您最好的选择是只使用type assertion或类似的overload签名。这种条件类型的好处是给调用者,而不是实现者。

如果您将TypeScript 3.5与smarter union type checking配合使用,则可以像上面这样修改您的代码:

T

export const getApiAsyncStatus = <T extends ApiAsyncStatus["_type"]>( registry: Registry, id: string, type: T, ) => { let status = registry[id]; if (status !== undefined && status._type !== type) { // annotate as error state const errorStatus: Extract<ApiAsyncStatus, { state: "error" }> = { _type: type as ApiAsyncStatus["_type"], // widen to concrete union error: new Error(`Expected _type ${type}, but received ${status._type}`), id, state: "error" }; status = errorStatus; // this assignment is okay } // still need this assertion return status as ExtractAsyncStatusByType<T, ApiAsyncStatus> | undefined; }; type的{​​{1}}的扩展将其从通用类型(缺乏编译器的演绎技能)改变为具体的并集(更好)。为了使编译器理解T类型的值可分配给ApiAsyncStatus["_type"]类型的变量,必须使用TS3.5中的更智能的联合检查。对于TS3.4及以下版本,编译器would not do such analysis at all。因此,即使是上述版本,在那些早期版本中仍然会出错。

要支持这些内容,您还可以使您的断言更宽泛,输入类型更安全:

{_type: A | B, error: Error, state: "error"}

Link to code

因此,根据您使用的TypeScript的版本,这两种方法均应工作。我倾向于将此问题视为与correlated types相关的一般问题,如果您可以convince the compiler多次键入检查代码块的次数,每次缩小一些范围,事情都会顺利进行联合类型的变量。对于您的情况,对于{_type: A, error: Error, state: "error"} | {_type: B, error: Error, state: "error"}(此处为export const getApiAsyncStatus = <T extends ApiAsyncStatus["_type"]>( registry: Registry, id: string, type: T, ) => { let status = registry[id]; if (status !== undefined && status._type !== type) { status = { _type: type, error: new Error(`Expected _type ${type}, but received ${status._type}`), id, state: "error" } as Extract<ApiAsyncStatus, { state: "error", _type: T }>; // assert as error type } // still need this assertion return status as ExtractAsyncStatusByType<T, ApiAsyncStatus> | undefined; }; T)的每个可能值,您的代码都应检查正常。但是,当“一次”查看联合或联合的通用扩展时,编译器认为某些被禁止的情况是可能的,并且不予理and。恐怕没有很好的答案。我的建议是断言您的出路并继续前进。

好的,希望能有所帮助;祝你好运!