Exploring TypeScript's ability to overload union types

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:

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 {
  ...
}

Answer №1

Unlike nominal typing, Typescript follows a structural approach, meaning that if two types have the same structure but different names, they are considered identical. In the case of WebGLTexture and WebGLBuffer, they seem to share the same structure.

If you use cmd+click (or ctrl+click) on these types in VSCode or the Typescript playground, you'll see their declarations:

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

Both types are essentially declared as unmodified WebGLObjects, which confuses Typescript as it cannot differentiate between them. Consequently, when attempting to invoke the second function overload, Typescript defaults to the first one since it matches closely.

This code snippet makes it challenging to propose a precise solution, but perhaps accepting objects with distinct keys could provide clarity for the overloads:

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

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

The use of the || operator within arguments

I am facing a challenge: //this console log displays correct value console.log('localstorage', localStorage.getItem('subMenu')); setSubMenu( JSON.parse(localStorage.getItem('subMenu') || JSON.stringify(s ...

Using TypeScript to implement getter/setter functionality on an array property

Is there a better approach for implementing getter/setter pattern with array properties? For instance: export class User { private _name: string; set name(value: string) { this._name = value; } get name(): string { return this._name; ...

Typescript: Firebase App type does not include delete, installations, name, or options properties

Exploring the realm of Typescript and its compatibility with Firebase has been a recent endeavor of mine. I've created a FirebaseProvider that requires a Firebase app to be configured in the following manner: import firebase from "firebase/app&qu ...

Expanding code lines beyond 80 characters in Visual Studio Code with TSLint integration

I am utilizing TypeScript alongside TSLint and Prettier within Visual Studio Code for developing a React Native App. Despite my efforts to set up my editor to automatically wrap the code per line at 100 characters, it consistently reverts back to 80 chara ...

Resolving a Type Error in NextAuth JWT Callback with Facebook OAuth and Next.js (Using TypeScript)

Currently, I am integrating Facebook authentication into my project and attempting to retrieve the access token in order to make API calls. This task is being completed in a Next.js TypeScript environment using NextAuth. pages/api/auth/[...nextAuth].ts: . ...

What steps should I take to build a TypeScript project (using tsdx) while excluding the source files from the final output?

My goal is to convert a typescript project into javascript, but I'm encountering an issue where the project's source files are ending up in the dist folder. I suspect it may be related to my configuration settings, as I have reviewed the document ...

How can I add Leaflet.TextPath or Leaflet.PolylineDecorator to an Angular 6 CLI application?

Struggling to integrate https://github.com/makinacorpus/Leaflet.TextPath into my Angular 6 CLI application without any luck. Any guidance would be greatly appreciated. Thank you in advance. UPDATE: Alternatively, what about this one? https://github.com/bb ...

A guide on verifying the static characteristics of a class with an interface

When it comes to converting a constructor function in JavaScript to TypeScript, there are some important considerations to keep in mind. function C() { this.x = 100; } C.prototype = { constructor: C, m() {} }; C.staticM = function () {}; Here ...

angular-cli encounters an issue: attempting to access a property that does not exist on type 'void'

Currently in the process of migrating an Angular 2 project to Angular 4 and also updating the angular-cli to the latest version 1.0. Unfortunately, there is no Ahead-of-Time compilation (AOT) implemented in the application. An issue has arisen with a comp ...

The function `readFile` is missing in the object `hostOrText`, resulting

I'm currently attempting to utilize npm rollup in order to convert my repository into an npm module, but I keep encountering the following error: [!] (plugin commonjs--resolver) TypeError: hostOrText.readFile is not a function at readJsonOrUndefin ...

Sending a parameter through a route to a child component as an input in Angular 2

My parent component receives an id value from a route parameter and I need to pass this value to a child component using the Input() decorator. The issue I'm facing is that I can't seem to get the route param value to be passed to the child compo ...

Retain Form data even when the user refreshes or reloads the page

My Form includes validation as shown below: <form [formGroup]="LoginForm"> First name:<br> <input type="text" formControlName="firstname" > <br> Last name:<br> <input type="text" formControlName="lastname"> ...

A step-by-step guide on retrieving the present date and time using TypeScript

Details This is my first time creating a VSCode extension using TypeScript, and I am having trouble displaying the current date/time in an information message. My Attempts I have searched through VSCode key bindings for any references to date/time, as w ...

<Angular Command Line Interface> Implement a script within an HTML file that takes a parameter from a separate .ts file

Hey there! Sorry if it's a bit unclear, but here's the issue: I have a file called "heroes.ts" with numerous objects for a "Hero" class (exported from another file), and here is a snippet of it --> import { Hero, Villain } from '../her ...

What could be causing the presence of a "strike" in my typescript code?

While transitioning my code from JavaScript to TypeScript for the first time, I noticed that some code has been struck out. Can someone explain why this is happening and what it signifies? How should I address this issue? Here's a screenshot as an exa ...

The generated code is unable to import the file compiled by Clasp

I am encountering an issue with my TypeScript files in my Google App Project. Here is a breakdown of my files: src/main.ts function main(): void { console.log('main'); hello(); } src/other.ts console.log('hello world'); ...

D3 event: Latest in Type Definitions for Version 6 and Beyond

I have a collection of widgets created using d3 (version 5) and now I need to transition them to version 7. After consulting the migration guide at , I discovered the updated syntax for events: // previously element.on('click', (d, i, e) => .. ...

I am attempting to store the primary array in local storage, but unfortunately, the value is not being saved within the React context API

I attempted to store the main array in local storage and retrieve it as global state, but I am facing an issue where the data is not being saved in the local storage. This file represents my context. import { createContext, useReducer, ReactNode, FC, use ...

Angular 1.5 Karma unit test causes duplicate loading of ng-mock library

My current web app is built using Typescript 2.4.2 and compiled with the latest Webpack version (2.7.0). I am in the process of incorporating Karma tests utilizing Jasmine as the assertion library. Below is my karma configuration file: 'use strict& ...

Using Generic Types in TypeScript for Conditional Logic

To better illustrate my goal, I will use code: Let's start with two classes: Shoe and Dress class Shoe { constructor(public size: number){} } class Dress { constructor(public style: string){} } I need a generic box that can hold either a ...