An interface in TypeScript that includes a method specifically designed to return exactly one type from a union of types

In my React function component, I have a generic setup where the type T extends ReactText or boolean, and the props include a method that returns a value of type T.

import React, { FC, ReactText } from 'react';

interface Thing {}

interface Props<T extends ReactText | boolean> {
  value: T | null;
  mapValues(thing: Thing): T;
}

interface MyComponent<T extends ReactText | boolean> extends FC<Props<T>> {}

function MyComponent<T extends ReactText | boolean>(
  { value, mapValues }: Props<T>
) {
  // implementation here
  return <div />;
}

However, I want to ensure that the method only returns either a ReactText or a boolean, not both. It should not be able to return a different type.

import React, { FC, ReactText } from 'react';

interface Thing {
  someBool: boolean;
  someStr: string;
}

interface Props<T extends ReactText | boolean> {
  value: 
    boolean extends T ? boolean | null : 
    ReactText extends T ? ReactText | null : 
    never;

  mapValues(thing: Thing): 
    boolean extends T ? boolean : 
    ReactText extends T ? ReactText :
    never;

  things: Thing[];
}

interface MyComponent<T extends ReactText | boolean> extends FC<Props<T>> {}
function MyComponent<T extends ReactText | boolean>(
  { value, mapValues, things }: Props<T>
) {
  if (value != null) {
    return <div>{value}</div>;
  }

  const items = things
    .map(mapValues)
    .map(v => <li key={v.toString() /** assuming v is unique */}>{v}</li>);

  return <ul>{items}</ul>;
}

Even though it works, the solution seems convoluted and potentially fragile.

const things: Thing[] = [
  { someStr: 'a', someBool: false },
  { someStr: 'b', someBool: true }
];

const value = null;
const mapValues = (thing: Thing) => 
  things.find(t => t.someStr === thing.someStr)?.someStr ?? 
  things.find(t => t.someBool === thing.someBool)?.someBool ??
  false;

// This code works but feels clumsy and may break easily
// @ts-expect-error
<MyComponent things={things} value={value} mapValues={mapValues} />;

// This alternative works
// Why do we need `as ReactText` in this scenario?
<MyComponent things={things} value={'abc' as ReactText} mapValues={() => 'abc' as ReactText} />;
Type '(thing: Thing) => string | boolean' is not assignable to type '(thing: Thing) => boolean'.  
  Type 'string | boolean' is not assignable to type 'boolean'.  
    Type 'string' is not assignable to type 'boolean'.

The current solution isn't very elegant and seems error-prone.

Is there a way to implement an exclusive OR (XOR) type without relying on interface properties? Maybe leveraging Extract<T, U>?

Try out this code snippet on the TS Playground.

Answer №1

Encountering the issue where generic type parameters face difficulty being constrained to a specific set of types is a common challenge. For more details on this limitation, refer to this link and also check out microsoft/TypeScript#27808 for a relevant feature request.

When dealing with a scenario where you have a generic type parameter T that needs to be limited to a selection of three types - A, B, and C, using an approach like T extends (A | B | C) might seem logical but falls short in practice. This method allows certain types that should be restricted, leading to ambiguity and inefficiency in constraint enforcement.

The ideal constraint would resemble something like T extends_oneof {A, B, C} or maybe

(T extends A) | (T extends B) | (T extends C)
; however, such direct syntax is not supported in TypeScript. Therefore, a workaround involves creating a type function called OneOf<T, C> to determine if T aligns with any of the specified constraints from tuple C.

// Sample implementation of OneOf type function
type OneOf<T extends C[number], C extends readonly any[]> =
  Extract<{ [I in keyof C]-?: [T] extends [C[I]] ? T : never }[number], T>;

// Usage examples
type O = OneOf<"abc", [string, number, boolean]>; // "abc"
type P = OneOf<123, [string, number, boolean]>; // 123
type Q = OneOf<"abc" | 123, [string, number, boolean]>; // never
... (remaining content unchanged for brevity) ...

Answer №2

(update: upon further investigation, it has come to my attention that both boolean and ReactText are considered union types themselves, which poses a problem for the proposed solution in your specific scenario)

Presented here is a type that will result in an evaluation of never for union types.

type NoUnionTypes<T> = [T] extends infer TTuple // preserves T as a unit
  // distributive conditional statement, disassembles T if it's a union
  ? T extends infer U  
    ? TTuple extends [U] ? T : never 
    : never 
  : never;

(admittedly, there may be a more fitting name for this construct)

By utilizing this type, we can define your Props interface as follows:

interface Props<T extends ReactText | boolean> {
  value: NoUnionTypes<T> | null;
  mapValues(thing: Thing): NoUnionTypes<T>;
  things: Thing[];
}

Explore on TS 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

When typing declarations are used, they clarify whether the entity being referenced is an Object or

I am currently working on aligning the Dockerode run typings to match the actual implementation. The issue arises when I invoke run as TypeScript consistently identifies the return value as a Promise. It seems that TypeScript is having trouble distinguish ...

What sets apart handcrafting Promises from utilizing the async/await API in practical terms?

I came into possession of a codebase filled with functions like the one below: const someFunc = async (): Promise<string> => { return new Promise(async (resolve, reject) => { try { const result = await doSomething(); ...

I am experiencing issues with the Link component in Next.js version 13.4, as it

Whenever I attempt to navigate by clicking on the buttons labeled About, Experience, and others, the page does not redirect me accordingly. Oddly enough, if I manually input the endpoint for that specific page like http://localhost:3000/#about, it function ...

Optimizing the sorting of object properties based on specific values (numbers or strings)

My goal is to simplify the process of sorting both number and string values. The first step involves checking if the parameter I've passed (which belongs to the DeliveryDetailsColumns constants) matches another parameter from a different type (Electro ...

Can you suggest a way to revise this in order to include the type of property (string)?

Here is a snippet of working code that raises a question about refactoring to improve the readability and functionality. Consider renaming the method to isPropValueSame. import * as _ from 'lodash'; const diff = _.differenceWith(sourceList, comp ...

How to extract multiple literals from a string using Typescript

type Extracted<T> = T extends `${string}${'*('}${infer A}${')+'}${string}${'*('}${infer A}${')+'}${string}` ? A : never type Result1 = Extracted<'g*(a12)+gggggg*(h23)+'> // 'a12' | &a ...

Difficulty in retrieving template variable within mat-sidenav-content

I am attempting to change a css class by targeting a div with a template variable within mat-sidenav-content. I have tried using both @ViewChild and @ContentChild but neither of them is able to retrieve the reference of the specified div at runtime. Below ...

Angular: Trigger service call upon onBlur event of input changes

In Angular, I am looking to detect any changes in the text input during the onBlur event and then take specific actions accordingly: Criteria for processing during the onBlur event: Only proceed if there has been a change in the text input. If the input ...

What is the process for importing an untyped Leaflet plugin into an Angular application?

I am trying to incorporate the leaflet plugin leaflet-mapwithlabels into my angular project. However, the library does not provide an option for NPM installation. After following some guides, I attempted adding the JS file directly to the node-modules in i ...

Error: Unable to assign the 'schedule' property to a null value

I'm currently developing a scheduling application using React.js and have implemented a draggable scheduling feature for users to indicate their availability. Everything seems to be working smoothly, except for one pesky error message: TypeError: Cann ...

What is the process for generating an object type that encompasses all the keys from an array type?

In my coding journey, I am exploring the creation of a versatile class that can define and handle CRUD operations for various resources. The ultimate goal is to have a single generic class instance that can be utilized to generate services, reducer slices, ...

Exploring the realm of Typescript custom decorators: The significance behind context

I'm currently working on a custom decorator that will execute decorated functions based on RxJS events. Everything seems to be going well so far, but I'm facing an issue when the function is executed: the context of the this object is lost. I&a ...

Create a new function and assign it to "this" using the button inside ngFor loop

I am working with a li tag that has a *ngFor directive: <li *ngFor="let items of buttons"> <button (click)="newMap(items.id, $event)"> {{ items.name }} </button> </li> The buttons array looks like this: buttons = [ {nam ...

Using a Type Guard in Typescript to check if an environment variable matches a key in a JSON object

I am currently working on creating a Type Guard to prevent TypeScript from throwing an error on the final line, where I attempt to retrieve data based on a specific key. TypeScript is still identifying the environment variable as a string rather than a rec ...

Tips for navigating the material ui Expanded attribute within the Expansion Panel

After looking at the image provided through this link: https://i.stack.imgur.com/kvELU.png I was faced with the task of making the expansion panel, specifically when it is active, take up 100% of its current Div space. While setting height: 100% did achi ...

The plugin called typescript from Rollup is throwing an error message with code TS2307. It says it cannot locate the module named 'App.svelte' or its related type declarations

I'm encountering a specific issue with my svelte project main.ts import App from './App.svelte'; const app = new App({ target: document.body, }); export default app; The first line is triggering a warning message Plugin typescript: @ ...

What could be the reason why my focus and blur event listener is not being activated?

It seems like everything is in order here, but for some reason, the event just won't fire... const element = (this.agGridElm.nativeElement as HTMLElement); element.addEventListener('focus', (focusEvent: FocusEvent) => { element.classLi ...

After filling a Set with asynchronous callbacks, attempting to iterate over it with a for-of loop does not accept using .entries() as an Array

Encountering issues with utilizing a Set populated asynchronously: const MaterialType_Requests_FromESI$ = SDE_REACTIONDATA.map(data => this.ESI.ReturnsType_AtId(data.materialTypeID)); let MaterialCollectionSet: Set<string> = new Set<s ...

Retrieve all exports from a module within a single file using Typescript/Javascript ES6 through programmatic means

I aim to extract the types of all module exports by iterating through them. I believe that this information should be accessible during compile time export const a = 123; export const b = 321; // Is there a way to achieve something similar in TypeScript/ ...

Learn how to easily toggle table column text visibility with a simple click

I have a working Angular 9 application where I've implemented a custom table to showcase the data. Upon clicking on a column, it triggers a custom modal dialog. The unique feature of my setup is that multiple dialog modals can be opened simultaneously ...