Home > Back-end >  Typescript: Type can be either/or
Typescript: Type can be either/or

Time:11-16

I have the following interface:

export interface Channel {
  version: string;

  render<T extends Record<string, unknown>>(payload: Payload, options?: T): Promise<Response>;
  render<T extends Record<string, unknown>>(context: IContext, options?: T): Promise<string>;
}

there are two types of Channels (web or nonWeb) that can be rendered. If I try to render one type of channel like this:

export interface web extends Channel {
  render(payload: Payload, options?: IWebChannelOptions): Promise<Response>;
}

I get Interface 'web' incorrectly extends interface 'Channel' because it's incompatible with the other type. The same happens when I try to call the nonWeb channel. What is the best way to resolve this?

CodePudding user response:

I suspect that your error is not just because your implementation wouldn't work for both Payload and IContext in the first argument, but also because you are restricting T beyond what the interface allows.

Channel's interface definition is a contract: You are promising that regardless of its implementing class, every Channel can be called with the caller's choice of render(Payload, T?) and render(IContext, T) for the caller's choice of T. That's what would allow you to interact with Channel as an interface without knowing its exact implementation. The fact that the name is the same is irrelevant to this contract: If Channel's methods were named a and b, you couldn't get away with only implementing a and leaving b, or it would break the contract that every conforming Channels can be called both ways.

The most obvious answer is to remove render from the Channel definition, then test which implementation you have before interacting with it.

That invites the question: Setting aside version, if Channel's two implementations don't share any useful method signatures, is there any reason for Channel to exist as an interface rather than type Channel = Web | NonWeb? What could you possibly pass render without figuring out whether the channel is web or nonWeb? This isn't as much of a downside in TypeScript as it might be if you're coming from a language like Java: If both Web and NonWeb declared version: string, TypeScript's structural typing system would correctly infer that Web | NonWeb contains a version: string without you having to declare it.

If you're sure you want an array of Channel, you can check to TypeScript's satisfaction either by checking instanceof web or instanceof nonWeb, by using a discriminated union, or by writing your own custom type predicate available on Channel (isWeb(): this is web) that you call instead of instanceof.

CodePudding user response:

I don't know if it's an option, but you could make Channel completely generic and then use type parameters in Web, maybe something like this?

export interface Channel<Input = unknown, Output = unknown> {
  version: string;
  render(context: Input, options?: Record<string, unknown>): Promise<Output>;
}

type TestOpts = { a: number };

export interface Web extends Channel<string, string> {
  render(payload: string, options?: TestOpts): Promise<string>;
}
  • Related