I need to create a function f
where the arguments and return types are union types that are connected, meaning when you call f(anInputType)
, you will receive aCorrespondingOutputType
. I experimented with TypeScript 4.1.3 and came up with the following solution:
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 // This compiles because the compiler recognizes that output is of type Output2
In real-life scenarios, the inputs should be WebGL objects. However, replacing 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 // This does not compile as the compiler assumes output is of type Output1
I'm trying to understand this issue by referring to various TypeScript sources and questions:
- Highlight on TypeScript Issue #27131
- Discussion on TypeScript Issue #14107
- Typescript Overloading Functions with Union Types - Stack Overflow
- Typescript Function Overloads Not Working for the Union Type Case - Stack Overflow
The difference in behavior between using existing types (like WebGL objects) and custom classes like WebGLTexture and WebGLBuffer puzzles me.
Following Alex's Approach
This approach may seem verbose, but it allows for a single implementation, which is beneficial considering other parts of the application structure. The code might appear messy somewhere, but this solution works well for now. Thank you!
Below is the actual implementation in the code:
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 {
...
}