打字稿狭窄的阶级成为一个歧视的联盟

时间:2019-07-07 12:48:36

标签: typescript discriminated-union

我很难将一个类的实例缩小到其受歧视的联合。

我具有以下已区别的工会:

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 个答案:

答案 0 :(得分:1)

问题是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编译器的状态意图,但是您需要确保函数主体中的代码能够执行声明的内容。