Фильтрация распознаваемого объединения TypeScript завершается неудачно для распознаваемого ключа - PullRequest
0 голосов
/ 02 июня 2019

У меня есть следующий код, который я использую для отслеживания состояний асинхронных запросов. Он использует _type в качестве дискриминатора, а также status.

В следующем коде я определяю два типа AsyncStatus: LoginAsyncStatus и SearchAsyncStatus. Они отличаются на _type и success 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 голосов
/ 03 июня 2019

Я повторю свои комментарии и продолжу дальше:

Дискриминационные союзы действительно работают так, как вы ожидаете, с конкретными типами, а не с дженериками. Кроме того, внутри реализации getApiAsyncStatus() тип T является неразрешенным универсальным параметром, и компилятор не делает много работы, пытаясь проверить, можно ли присвоить значение условному типу, зависящему от такого неразрешенного универсального. Лучше всего здесь просто использовать утверждение типа или что-то эквивалентное, например overload подпись. Преимущество этого условного типа для абонентов, а не для разработчиков.

Если вы используете TypeScript 3.5 с более умной проверкой типа объединения , вы можете исправить приведенный выше код следующим образом:

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 с T до ApiAsyncStatus["_type"] изменяет его с универсального типа (для которого отсутствуют дедуктивные навыки компилятора) на конкретный союз (что лучше). Более разумная проверка объединения в TS3.5 необходима для того, чтобы компилятор понимал, что значение типа {_type: A | B, error: Error, state: "error"} присваивается переменной типа {_type: A, error: Error, state: "error"} | {_type: B, error: Error, state: "error"}. Для TS3.4 и ниже, компилятор вообще не будет выполнять такой анализ . Так что даже вышеупомянутое будет все еще ошибкой в ​​тех более ранних версиях.

Чтобы поддержать тех, кого вы могли бы также сделать своими утверждениями шире и менее безопасными:

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;
};

Ссылка на код

Так что любой из них должен работать в зависимости от используемой вами версии TypeScript. Я склонен думать об этой проблеме как об общем классе проблем, связанных с коррелированными типами , где все будет работать нормально, если вы сможете убедить компилятор проверить тип блока, кратного множественному коду времена для каждого возможного сужения некоторой переменной типа объединения. В вашем случае для каждого возможного значения T ("LOGIN" и "SEARCH" здесь) ваш код должен проверяться нормально. Но, глядя на союзы или общие расширения союзов «все сразу», компилятор думает, что некоторые запрещенные ситуации возможны, и отказывается. Боюсь, нет лучшего ответа на этот вопрос ... мой совет - просто утвердить свой выход и идти дальше.

Хорошо, надеюсь, это поможет; удачи!

...