[Перевод] Когда «as never» — единственное, что работает

4c52c2d07c8741c293665c2991a86742.jpg

Эта статья — перевод оригинальной статьи «When 'as never' Is The Only Thing That Works».

Также я веду телеграм канал «Frontend по-флотски», где рассказываю про интересные вещи из мира разработки интерфейсов.

Вступление

as never, очень редко требуется в TypeScript. Давайте рассмотрим пример, где это необходимо.

Представим, что мы хотим отформатировать некоторый ввод на основе его typeof. Сначала мы создадим объект formatters, который сопоставит typeof с функцией форматирования:

const formatters = {
  string: (input: string) => input.toUpperCase(),
  number: (input: number) => input.toFixed(2),
  boolean: (input: boolean) => (input ? "true" : "false"),
};

Далее мы создадим функцию format, которая будет принимать на вход string | boolean | number и форматировать ее в зависимости от ее typeof.

const format = (input: string | number | boolean) => {
  // Здесь нам нужно сделать кастинг, потому что TypeScript не совсем умен.
  // достаточно знать, что `typeof input` может быть только
  // 'string' | 'number' | 'boolean'
  const inputType = typeof input as
    | "string"
    | "number"
    | "boolean";
  const formatter = formatters[inputType];
 
  return formatter(input);
/*
Argument of type 'string | number | boolean' is not assignable to parameter of type 'never'.
  Type 'string' is not assignable to type 'never'.
*/
};

Но возникает странная ошибка:

Type 'string' is not assignable to type 'never'.

Что здесь происходит?

Объединения функций с несовместимыми параметрами

Давайте подробнее рассмотрим тип formatter внутри нашей функции format:

const format = (input: string | number | boolean) => {
  const inputType = typeof input as
    | "string"
    | "number"
    | "boolean";
  const formatter = formatters[inputType]; // ((input: string) => string) | ((input: number) => string) | ((input: boolean) => "true" | "false")
            
  return formatter(input);
};

Как вы видите, она представляет собой объединение функций, каждая из которых имеет свой параметр. Одна функция принимает строку, другая — число, а последняя — булево.

Как мы можем вызвать эту функцию со строкой и числом одновременно? Никак. Поэтому функция на самом деле разрешается так:

type Func = (input: never) => string;

Разве параметры не должны привести к объединению?

Вы можете подумать: «Разве параметры не должны преобразовываться в объединение string | number | boolean?».

Это не работает, потому что вызов formatters.string с числом небезопасен. Вызов formatters.boolean с числом небезопасен.

Таким образом, never — единственный тип, который имеет смысл.

Как это исправить?

Мы знаем, что логика этой функции верна. Мы знаем, что formatters[inputType] преобразуется в правильный тип.

Поэтому мы можем использовать as never:

const format = (input: string | number | boolean) => {
  const inputType = typeof input as
    | "string"
    | "number"
    | "boolean";
  const formatter = formatters[inputType];
 
  return formatter(input as never);
};

Это заставляет TypeScript рассматривать input как тип never — который, конечно же, можно присвоить параметру форматера never.

Не будет ли работать as any?

Удивительно, но здесь any не работает:

const format = (input: string | number | boolean) => {
  const inputType = typeof input as
    | "string"
    | "number"
    | "boolean";
  const formatter = formatters[inputType];
 
  return formatter(input as any);
  // Argument of type 'any' is not assignable to parameter of type 'never'.
};

Это приводит к чудовищной ошибке:

Argument of type 'any' is not assignable to parameter of type 'never'

Так что, as never это единственный выход.

© Habrahabr.ru