简体   繁体   中英

Overloading union types in TypeScript

I want to define a function f such that arguments and return types are union types but they are tied, ie when you call f(anInputType) you get aCorrespondingOutputType as a result. I tried the following at it seems to work on TypeScript 4.1.3.

class Input1 {
    type1Input: string = "I am type 1!";
}

class Input2 {
    type2Input: string = "I am type 2!";
}

interface Output1 {
    type1Output: string;
}

interface Output2 {
    type2Output: string;
}

export type Input = Input1 | Input2;
export type Output = Output1 | Output2;

function f(_input: Input1): Output1;
function f(_input: Input2): Output2;
function f(_input: Input): Output {
    return null as any as Output;
}

const input = new Input2();

const output = f(input);
output.type2Output // Compiles because the compiler can figure out that output is of type Output2

However, in (my) real life the inputs should be WebGL objects. The same thing, in which I replaced Input1 with WebGLTexture and Input2 with WebGLBuffer , does not compile.

interface Output1 {
    type1Output: string;
}

interface Output2 {
    type2Output: string;
}

export type Output = Output1 | Output2;

function f(_input: WebGLTexture): Output1;
function f(_input: WebGLBuffer): Output2;
function f(_input: WebGLTexture | WebGLBuffer): Output {
    return null as any as Output;
}

const canvas = document.createElement("canvas")!;
const gl = canvas.getContext("webgl")!;
const input = gl.createBuffer()!;

const output = f(input);
output.type2Output // Does not compile because the compiler thinks that output is of type Output1

Am I missing something?

I am reading-ish through these TypeScript issues:

As well as these other questions:

But what troubles me is the difference between the behavior using existing types ( ie the WebGL objects) versus custom classes.

Actually, I am adopting Alex idea

It looks verbose but with it I can get away with a single implementation. It's kinda of a no-win for me because of the way other parts of the app are organized; something, somewhere, is going to look ugly. But this is good for now! Thanks!

Real-life code below.

export type WebGLResource =
  { texture: WebGLTexture } |
  { buffer: WebGLBuffer } |
  { program: WebGLProgram } |
  { renderbuffer: WebGLRenderbuffer } |
  { framebuffer: WebGLFramebuffer };
export type ResourceMeta = TextureMeta | BufferMeta | ProgramMeta | RenderbufferMeta | FramebufferMeta;

function getMeta(<...omitted params...> resource: { texture: WebGLTexture }, required: true): TextureMeta;
function getMeta(<...omitted params...> resource: { buffer: WebGLBuffer }, required: true): BufferMeta;
function getMeta(<...omitted params...> resource: { program: WebGLProgram }, required: true): ProgramMeta;
function getMeta(<...omitted params...> resource: { renderbuffer: WebGLRenderbuffer }, required: true): RenderbufferMeta;
function getMeta(<...omitted params...> resource: { framebuffer: WebGLFramebuffer }, required: true): FramebufferMeta;
function getMeta(<...omitted params...> resource: { texture: WebGLTexture }, required: false): TextureMeta | null;
function getMeta(<...omitted params...> resource: { texture: WebGLBuffer }, required: false): BufferMeta | null;
function getMeta(<...omitted params...> resource: { buffer: WebGLProgram }, required: false): ProgramMeta | null;
function getMeta(<...omitted params...> resource: { renderbuffer: WebGLRenderbuffer }, required: false): RenderbufferMeta | null;
function getMeta(<...omitted params...> resource: { framebuffer: WebGLFramebuffer }, required: false): FramebufferMeta | null;
function getMeta(<...omitted params...> resource: WebGLResource, required: boolean): ResourceMeta | null {
  ...
}

Typescript is structural, not nominal. That means that if two type have the same shape, but different names, they are considered the same type. And unfortunately, WebGLTexture and WebGLBuffer appears to have the same shape.

If you cmd+click (or ctrl+click) on either of those types in VSCode or the typescript playground, you can see how they are declared. That yeilds these two declarations:

interface WebGLTexture extends WebGLObject {}
interface WebGLBuffer extends WebGLObject {}

Sadly, both type are simply declared as unaltered WebGLObject s. Typescript cannot tell them apart. So when you try to trigger the second function overload, typescript notices that the first overload matches, and just uses that.

It's hard to advise a better solution from this contrived code, but you probably will want a different approach here.


Maybe you could instead accept an object with keys that make it clear what you're passing in? That way the different overloads and have different argument types.

function f(_input: { texture: WebGLTexture }): Output1;
function f(_input: { buffer: WebGLBuffer }): Output2;
function f(_input: { texture?: WebGLTexture, buffer?: WebGLBuffer }): Output {
    return null as any as Output;
}

const canvas = document.createElement("canvas")!;
const gl = canvas.getContext("webgl")!;

const buffer = gl.createBuffer()!;
const texture = gl.createTexture()!;

f({ texture }).type1Output; // works
f({ buffer }).type2Output; // works

Playground

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM