Infer Typescript функция возвращает тип из параметра enum - PullRequest
1 голос
/ 02 апреля 2020

Я хочу создать службу загрузки, которая возвращает правильно введенные данные для идентификаторов, определенных в перечислении. То, что я сделал, выглядит следующим образом:

enum IdentifierEnum {
  ID1 = 'ID1', 
  ID2 = 'ID2'
}

interface DataType {
  [IdentifierEnum.ID1]: number,
  [IdentifierEnum.ID2]: string
}

class LoadingService {
  loadData<K extends IdentifierEnum>(key: K): DataType[K] {
    // ...
  }
}

Используя этот подход, типы правильно выводятся при использовании службы загрузки:

const loadingService: LoadingService = new LoadingService();
const data1 = loadingService.loadData(IdentifierEnum.ID1); // type: number
const data2 = loadingService.loadData(IdentifierEnum.ID2); // type: string

Единственная проблема, с которой я сталкиваюсь, это то, что внутри реализация loadData, параметр типа K выводится только как IdentifierEnum. Поэтому следующее не будет работать:

class LoadingService {
  loadData<K extends IdentifierEnum>(key: K): DataType[K] {
    if (key === IdentifierEnum.ID1) {
      return 1; // Error: Type '1' is not assignable to type 'DataType[K]'.
    }
    // ..
  }
}

Для меня абсолютно логично, что это так. Тем не менее, я хотел бы иметь для этого полностью безопасное для типов решение.

Я уже пытался перегрузить функцию, но это оставляет меня с проблемой, что я все еще должен предоставить подпись реализации, которая либо слишком конкретна c (как и выше) или слишком общий, что опять же удаляет те типы безопасности, которые я желаю. То же самое относится и к приведению возвращаемого значения. В сущности, мне нужен способ действительно проверить тип входного значения, а не просто проверить его значение.

Есть ли возможность сделать это? Или, может быть, совершенно другим способом решения этой проблемы, обеспечивающим безопасность типов как для использования, так и для реализации службы загрузки?

Примечание

Реализация может показаться слишком сложной для этой простой цели, но причина этого в том, что существует базовый класс c для службы загрузки, который выглядит следующим образом:

// Base class
abstract class AbstractLoadingService<E extends string | number | symbol, T extends {[K in E]: any}> {
  abstract loadData<K extends E>(key: K): T[K];
}

// Implementation
class LoadingService extends AbstractLoadingService<IdentifierEnum, DataType> {
  loadData<K extends IdentifierEnum>(key: K): DataType[K] {
    // ...
  }
}

1 Ответ

1 голос
/ 02 апреля 2020

Это известная проблема в TypeScript, см. microsoft / TypeScript # 13995 . Проблема заключается в том, что анализ типа на основе потока управления не применяется к параметрам типа generi c.

Если бы key был объявлен как тип IdentifierEnum, то проверка if (key === IdentifierEnum.ID1) {...} сузила бы тип key внутри блока {...} до Identifier.ID1:

const k: IdentifierEnum = key;
if (k === IdentifierEnum.ID1) {
  k; // const k: IdentifierEnum.ID1
} else {
  k; // const k: IdentifierEnum.ID2
}

Это анализ потока управления. Теперь этого не происходит, когда key имеет тип c типа K, но даже если это так, это не поможет:

if (k === IdentifierEnum.ID1) {
  return 1; // ERROR! 
}

Это потому, что в TypeScript 3.8 в любом случае, даже если значение типа K сужено, сам тип K равен , а не . Компилятор никогда не говорит «если key равно IdentifierEnum.ID1, то K равно IdentifierEnum.ID1». Поэтому вы не можете использовать этот вид реализации на основе потока управления, если хотите, чтобы компилятор проверил безопасность типов для вас.

Возможно, что будущие версии TypeScript каким-то образом улучшат это, но это сложно , В общем, то, что значение x типа X может быть сужено до типа Y, не означает, что сам тип X может быть сужен. Это очевидно, если у вас есть несколько значений типа X. Но, во всяком случае, сейчас это что-то, чтобы обойти.


Вы уже изучили и были недовольны менее безопасными для типов способами сделать это: вводить утверждения и подписи с перегрузкой, поэтому я не буду писать, как бы вы это реализовали.


Единственный способ сделать что-то относительно безопасное для типов - это отказаться от анализа потока управления и вместо этого использовать операцию индексации. Компилятор достаточно умен, чтобы понять, что если у вас есть значение t типа T и значение k типа K extends keyof T, то значение t[k] будет иметь тип T[K]. В вашем случае T - это DataType. Таким образом, вам нужно значение этого типа для индексации:

class LoadingService {
  loadData<K extends IdentifierEnum>(key: K): DataType[K] {
    return {
      get [IdentifierEnum.ID1]() { return 1; },
      get [IdentifierEnum.ID2]() { return "" }
    }[key]; // okay
  }
}

Приведенный выше тип проверяет. Обратите внимание, что я реализовал свойства как getters . Вам не нужно делать это; Вы могли бы просто написать:

return {
  [IdentifierEnum.ID1]: 1,
  [IdentifierEnum.ID2]: ""
}[key];

, но версия геттера позволяет вам делать больше произвольных вычислений и знать, что будет оцениваться только тот, который соответствует key.


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

Детская площадка ссылка на код

...