Utilizing TypeScript to perform typing operations on subsets of unions

A TypeScript library is being developed by me for algebraic data types (or other names they may go by), and I am facing challenges with the more complex typing aspects.

The functionality of the algebraic data types is as follows:

// Creating ADT instatiator
const value_adt = adt({
    num: (value: number) => value,
    str: (value: string) => value,
    obj: (value: object) => value,
});

// Extracting union of variants
type ValueADT = Variants<typeof value_adt>;

// 'ValueADT' will be typed as:
// { [tag]: "num", value: number } |
// { [tag]: "str", value: string } |
// { [tag]: "obj", value: object }

// Creating instance of 'value_adt'
const my_value = value_adt.str("hello") as ValueADT;
...

...and a playground.

Update 2:
After this was answered, I managed to also correctly infer the return type of match (it returns the returned value from the called matcher). This is a bit of an aside with respect to what I asked here, but it might be interesting for anyone who comes across this in the future: playground.

Answer №1

The focus here is solely on the typings for the callers of the match() function, taking into account that the implementation may require type assertions or similar techniques to avoid compiler errors when dealing with complex generic call signatures.


One approach would be to create multiple versions of the match() function - one to handle all possible cases exhaustively and another to cater to partial matches along with default actions defined by matchers.

For the partial matching scenario, it's essential to make match() a generic function not only based on the union type T for the variant argument but also considering the keys K from the matchers object. To achieve precision in representing the argument types for all methods, it makes sense to introduce generics for both T and K within the Matchers structure.


A potential definition for Matchers<T, K> could look like this:

type Matchers<T extends Variant<string, any>, K extends PropertyKey> = {
    [P in K]: (value: P extends typeof def ?
        Exclude<T, { tag: Exclude<K, typeof def> }>["value"] :
        Extract<T, { tag: P }>["value"]
    ) => any };

This setup essentially maps over each element P in

K</code to generate callbacks returning values of <code>any
. Determining the type of the value parameter within the callback with key
P</code involves checking if <code>P
corresponds to the type of def, where the value represents variants not explicitly mentioned in K</code, or if <code>P is one of the tags from
T</code, in which case <code>value
corresponds to the specific variant value.

It's worth noting that if K encompasses the entire union of T["tag"], then the [def] callback will have a value argument of type

never</code, which though peculiar, should not cause any issues. If desired, the type can be altered so that the complete property is of type <code>never</code rather than just the callback argument.</p>
<hr />
<p>The overloaded call signatures for <code>match()
are as follows:

declare function match<T extends Variant<string, any>, K extends T["tag"] | typeof def>(
    variant: T, matchers: Matchers<T, K | typeof def>
): void;

declare function match<T extends Variant<string, any>>(
    variant: T, matchers: Matchers<T, T["tag"]>
): void;

The first signature is designed for handling partial matches with the inclusion of the def property. The inference process can be complex at times and specifying | typeof def in both the constraint for

K</code and within the second argument to <code>Matchers</code becomes necessary to ensure accurate inference of <code>K</code from the actual keys provided in the <code>matchers
argument.

The second signature caters to scenarios where an exhaustive match is needed without the presence of the def property, hence eliminating the need for K to be generic since it always aligns with the full T["tag"] union.


Testing against the following:

declare const adt:
    | Variant<"num", number>
    | Variant<"str", string>
    | Variant<"dat", Date>;

For the "match all" use case:

match(adt, {
    num: v => v.toFixed(),
    dat: v => v.toISOString(),
    str: v => v.toUpperCase()
});

Looks promising with the compiler understanding the types of v in each callback. Next, excluding the str key and introducing the [def] key:

match(adt, {
    num: v => v.toFixed(),
    dat: v => v.toISOString(),
    [def]: v => v.toUpperCase()
});

Compiler deduces that v must be of type

string</code for the default matcher. Further, omitting the <code>dat
key:

match(adt, {
    num: v => v.toFixed(),
    [def]: v => typeof v === "object" ? v.toISOString() :
        v.toUpperCase()
});

Types remain accurate with v now being of type

Date | string</code. Lastly, testing exclusively with the default matcher:</p>
<pre><code>match(adt, {
    [def]: v => typeof v === "number" ? v.toFixed() :
        typeof v === "object" ? v.toISOString() :
            v.toUpperCase()
})

The type of v expands to the complete

number | Date | string</code union. Experimenting with all tags present alongside the default matcher:</p>
<pre><code>const assertNever = (x: never): never => { throw new Error("Uh oh") };

match(adt, {
    num: v => v.toFixed(),
    dat: v => v.toISOString(),
    str: v => v.toUpperCase(),
    [def]: v => assertNever(v)
});

No issues identified, default matcher accepted, and v correctly assessed as type

never</code (since invoking the default matcher isn't expected).</p>
<p>Introducing mistakes to observe error messages - when <code>str
and the default matcher are omitted:

match(adt, {
    num: v => v.toFixed(),
    dat: v => v.toISOString(),
}); // error!
// No overload matches this call.
// Overload 1: Property '[def]' is missing
// Overload 2: Property 'str' is missing

Error message highlights failing to comply with either of the two call signatures and indicates that either [def] or str is absent. On mistyping a key:

// oops
match(adt, {
    num: v => v.toFixed(),
    dat: v => v.toISOString(),
    strr: v => v.toUpperCase(), // error, 
    //strr does not exist, Did you mean to write 'str'?
})

Description of unrecognized key and suggestions provided depending on how close the misspelling is to a valid key name.


In conclusion, by incorporating a second generic call signature focusing on matcher keys, achieving the expected type inference for default callback arguments can be facilitated.

Link to Playground code snippet

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

Angular 4 incorporates ES2017 features such as string.prototype.padStart to enhance functionality

I am currently working with Angular 4 and developing a string pipe to add zeros for padding. However, both Angular and VS Code are displaying errors stating that the prototype "padStart" does not exist. What steps can I take to enable this support in m ...

You are unable to apply 'use client' on a layout element in Next.js

While attempting to retrieve the current page from the layout.txt file, I encountered errors after adding 'use client' at the top of the page: Uncaught SyntaxError: JSON.parse: unexpected character at line 1 column 1 of the JSON data parseMod ...

Both undefined and null are sometimes allowed as values in conditional types, even when they should not be

Do you think this code should trigger a compiler error? type Test<T extends number | string> = { v: T extends number ? true : false } const test: Test<1> = { v: undefined } Is there something I am overlooking? Appreciate your help! ...

Locating Items in an Array using Angular 5 and Forming a New Array with the Located Objects

Looking for a way to extract objects from an array that have the type "noActiveServiceDashboard" and "extraAmountDashboard". I want to create a new array with only these two entries in the same format. I've attempted using .find() or .filter() method ...

The JsonFormatter is throwing an error because it is trying to access the property 'on' of an undefined variable

I have encountered an error while attempting to generate an HTML report using cucumber-html-reporter The error message is: Unhandled rejection TypeError: Cannot read property 'on' of undefined at new JsonFormatter (C:\path-to-project\ ...

An Unexpected ER_BAD_FIELD_ERROR in Loopback 4

I encountered an unusual error: Unhandled error in GET /managers: 500 Error: ER_BAD_FIELD_ERROR: Unknown column 'role_id' in 'field list' at Query.Sequence._packetToError (/Users/xxxx/node_modules/mysql/lib/protocol/se ...

Encountering SUID Sandbox Helper Issue When Running "npm start" on WSL with Electron and Typescript

Can anyone help me with this issue? I have Node v8.10.0 and I'm attempting to follow a beginner tutorial on Electron + Typescript which can be found at the following link: https://github.com/electron/electron-quick-start-typescript Here is the full e ...

Navigating in Angular with parameters without modifying the URL address

In a nutshell, my goal is to navigate to a page with parameters without showing them in the URL. I have two components: Component A and B. What I want to do is route to B while still needing some parameters from A. I know I can achieve this by setting a ...

Establishing the context for the input template by utilizing ng-template, ng-container, and ngTemplateOutlet

I am facing a challenge with a customizable component that utilizes an ng-container to display either a default template or a template passed in as an input. The issue arises when I try to set the context of the passed-in template to the nesting component ...

Navigating JSON data with unexpected fields in Typescript and React.js

Looking to parse a JSON string with random fields in Typescript, without prior knowledge of the field types. I want to convert the JSON string into an object with default field types, such as strings. The current parsing method is: let values = JSON.parse ...

Exploring the File Selection Dialog in Node.js with TypeScript

Is it possible to display a file dialog in a Node.js TypeScript project without involving a browser or HTML? In my setup, I run the project through CMD and would like to show a box similar to this image: https://i.stack.imgur.com/nJt3h.png Any suggestio ...

What is the best way to retrieve data from MySQL for the current month using JavaScript?

I need to retrieve only the records from the current month within a table. Here is the code snippet: let startDate = req.body.startDate let endDate = req.body.endDate let result = await caseRegistration.findByDate({ p ...

Modifying the values of various data types within a function

Is there a more refined approach to enhancing updateWidget() in order to address the warning in the else scenario? type Widget = { name: string; quantity: number; properties: Record<string,any> } const widget: Widget = { name: " ...

Tips for resolving the ExtPay TypeError when using Typscript and Webpack Bundle

I am currently trying to install ExtPay, a payment library for Chrome Extension, from the following link: https://github.com/Glench/ExtPay. I followed the instructions up until step 3 which involved adding ExtPay to background.js. However, I encountered an ...

Is the Angular Library tslib peer dependency necessary for library publication?

I have developed a library that does not directly import anything from tslib. Check out the library here Do we really need to maintain this peer dependency? If not, how can we remove it when generating the library build? I have also posted this question ...

What steps can I take to ensure TypeScript compiler approves of variance in calling generic handlers, such as those used in expressJS middleware?

disclaimer: I am a bit uncertain about variance in general... Here is the scenario I am facing: // index.ts import express from 'express'; import {Request, Response} from 'express'; const app = express(); app.use(handler); interface ...

Can a number value in a JSON object be converted to a string?

In my application, I have defined an interface: export interface Channel { canal: string; name: number; status: string; temperature: number; setpoint: number; permission: boolean; percentOut: number; } [UPDATE] in the HTML file: <input ...

Avoiding useCallback from being called unnecessarily when in conjunction with useEffect (and ensuring compliance with eslint-plugin-react-hooks)

I encountered a scenario where a page needs to call the same fetch function on initial render and when a button is clicked. Here is a snippet of the code (reference: https://stackblitz.com/edit/stackoverflow-question-bink-62951987?file=index.tsx): import ...

The cursor in the Monaco editor from @monaco-editor/react is not aligning with the correct position

One issue I am facing with my Monaco editor is that the cursor is always placed before the current character rather than after it. For example, when typing a word like "policy", the cursor should be placed after the last character "y" but instead, it&apos ...

Retrieve a collection of Firebase records using a specific query parameter

I'm currently developing a web app with Angular 2 and facing an issue with retrieving data from Firebase based on specific conditions in the child node. Here's how my Firebase structure looks like: I need to extract the entry for the one with app ...