Búsqueda de sitios web

Typescript estrecha la clase en una unión discriminada


Me está resultando difícil restringir una instancia de una clase a su unión discriminada.

Tengo la siguiente unión discriminada:

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"];

Y la siguiente clase:

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

Ahora, ¿cómo puedo limitar una instancia de esa clase a su respectivo ILoadableDiscriminated<T> unión manteniendo cierta seguridad de tipo (sin usar any)?

Por ejemplo, tengo el siguiente método create y me gustaría devolver la unión discriminada:

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

Se puede encontrar un ejemplo en: https://codesandbox.io/embed/weathered-frog-bjuh0 Archivo: src/DiscriminatedUnion.ts

Respuesta #1

El problema es que no existe relación entre Loadable<T> y las interfaces definidas que garanticen que la función createLoadable() establezca cada propiedad en el estado correcto antes de devolver el elemento. Por ejemplo, Loadable<string> podría tener estos valores:

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

Lo anterior no encaja en ninguna interfaz, pero es una instancia válida de Loadable.

Mi enfoque sería el siguiente:

Simplifica la interfaz, solo uno tiene que ser genérico:

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"];

Crea una clase separada para cada interfaz, para asegurar que los objetos creados cumplan con las definiciones de la interfaz:

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

Entonces podríamos usar la función con sobrecarga, para declarar la intención:

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

Finalmente, dependiendo de tus parámetros de entrada para la función createLoadable(), el tipo será discriminado automáticamente como tipo de retorno:

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)

Tenga en cuenta que las sobrecargas de parámetros indican la intención para el compilador de Typescript, pero necesita asegurarse de que el código en el cuerpo de la función haga lo que declaró.

Artículos relacionados