Машинопись узкого класса в дискриминационный союз - PullRequest
1 голос
/ 07 июля 2019

Мне трудно сузить экземпляр класса до его различного объединения.

У меня есть следующий дискриминируемый союз:

interface ILoadableLoading<T> {
  state: "Loading";
  id: number;
}

interface ILoadableLoaded<T> {
  state: "Loaded";
  id: number;
  item: T;
}

interface ILoadableErrored<T> {
  state: "Error";
  id: number;
  error: string;
}

export type ILoadableDiscriminated<T> =
  | ILoadableLoading<T>
  | ILoadableLoaded<T>
  | ILoadableErrored<T>;

type ILoadableState<T> = ILoadableDiscriminated<T>["state"];

И следующий класс:

class Loadable<T> {
  state: ILoadableState<T> = "Loading";
  id: number = 0;
  item?: T | undefined;
  error?: string | undefined;
}

Теперь, как я могу сузить экземпляр этого класса до соответствующего ILoadableDiscriminated<T> объединения, сохраняя некоторый тип безопасности (не используя)?

например. У меня есть следующий метод создания, и я хотел бы вернуть различенное объединение:

unction createLoadable<T>(someState: boolean): ILoadableDiscriminated<T> {
  var loadable = new Loadable<T>();

  if (someState) {
    loadable.state = "Error";
    loadable.error = "Some Error";

    // Would like to remove this cast, as it should narrow it out from state + defined error above
    return loadable as ILoadableErrored<T>;
  }

  if (loadable.state === "Loading") {
    // Would like to remove this cast, as it should narrow it from state;
    return loadable as ILoadableLoading<T>;
  }

  if (loadable.state === "Loaded" && loadable.item) {
    // Would like to remove this cast, as it should narrow it from state;
    return loadable as ILoadableLoaded<T>;
  }

  throw new Error("Some Error");
}

Образец можно найти на: https://codesandbox.io/embed/weathered-frog-bjuh0 Файл: src/DiscriminatedUnion.ts

1 Ответ

1 голос
/ 08 июля 2019

Проблема в том, что нет никакой связи между Loadable<T> и определенными интерфейсами, которые гарантировали бы, что функция createLoadable() устанавливает каждое свойство в правильное состояние перед возвратом элемента. Например, Loadable<string> может иметь следующие значения:

var loadable = new Loadable<string>();
loadable.state = "Error";
lodable.item = "Result text.";
return loadable;

Выше не подходит ни один интерфейс, но это действительный Loadable экземпляр.

Мой подход будет следующим:

Упрощенный интерфейс, только один должен быть универсальным:

interface ILoadableLoading {
  state: "Loading";
  id: number;
}

interface ILoadableLoaded<T> {
  state: "Loaded";
  id: number;
  item: T;
}

interface ILoadableErrored {
  state: "Error";
  id: number;
  error: string;
}

export type ILoadableDiscriminated<T> =
  | ILoadableLoading
  | ILoadableLoaded<T>
  | ILoadableErrored;

type ILoadableState<T> = ILoadableDiscriminated<T>["state"];

Создайте отдельный класс для каждого интерфейса, чтобы гарантировать, что созданные объекты придерживаются определений интерфейса:

class LoadableLoading implements ILoadableLoading {
  state: "Loading" = "Loading";
  id: number = 0;
}
class LoadableLoaded<T> implements ILoadableLoaded<T> {
  constructor(public item: T){}
  state: "Loaded" = "Loaded";
  id: number = 0;
}
class LoadableErrored implements ILoadableErrored {
  constructor(public error: string){}
  state: "Error" = "Error";
  id: number = 0;
}

Тогда мы могли бы использовать функцию с перегрузкой, чтобы указать намерение:

function createLoadable<T>(someState: true, state: ILoadableState<T>, item?: T): ILoadableErrored;
function createLoadable<T>(someState: false, state: "Loading", item?: T): ILoadableLoading;
function createLoadable<T>(someState: false, state: "Loaded", item?: T): ILoadableLoaded<T>;
function createLoadable<T>(someState: boolean, state?: ILoadableState<T>, item?: T): ILoadableDiscriminated<T> {
  if (someState) {
    return new LoadableErrored("Some error");
  }

  if (state === "Loading") {
    // Would like to remove this cast, as it hsould figure it out from state;
    return new LoadableLoading();
  }

  if (state === "Loaded" && item) {
    // Would like to remove this cast, as it hsould figure it out from state;
    return new LoadableLoaded(item);
  }

  throw new Error("Some Error");
}

Наконец, в зависимости от ваших входных параметров для функции createLoadable(), тип будет возвращен, тип будет различаться автоматически:

const lodableError = createLoadable<string>(true, "Loading");
console.log(lodableError.error);

const lodableLoading = createLoadable<string>(false, "Loading");
console.log("Loading");

const loadableLoaded = createLoadable<string>(false, "Loaded", "MyResponse");
console.log(loadableLoaded.item)

Обратите внимание, что параметр перегружает намерение состояния для компилятора Typescript, но вы должны убедиться, что код в теле функции выполняет то, что вы объявили.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...