Tips for steering clear of distributive conditional types

Here's a breakdown of the different types:

type Action<T> = T extends undefined ? {
    type: string;
} : {
    type: string;
    payload: T;
}

type ApiResponse<T> = {
    ok: false;
    error: string;
} | {
    ok: true;
    data: T;
};

This function has been defined:

function handleApiResponse<T>(apiResponse: ApiResponse<T>) {
    const a: Action<ApiResponse<T>> = {
        type: "response",
        payload: apiResponse,
    }
}

The issue arises because there is an error with variable a due to conditional type distribution within Action.

The goal for Action<T> is to have two specific cases:

  1. If the type parameter passed to Action<T> is undefined, then Action<undefined> should be equal to { type: string }
  2. If any other type parameter is passed to Action<T>, it should result in { type: string, payload: T }

However, this setup fails when T is a union type due to distributed conditional types.

Is there a way to create a type like this that functions correctly even when T is a union?

Answer №1

Your explanation refers to the concept of Action<T> as a distributive conditional type. In this scenario, unions within T are broken down into individual elements (e.g., B | C | D), processed through Action<T>, and then aggregated back into a new union (e.g.,

Action<B> | Action<C> | Action<D>
). Although this behavior is commonly desired in conditional types, it was used unintentionally in your case.

A conditional type structured like TTT extends UUU ? VVV : WWW will only be distributive if the type being evaluated, TTT, is a plain generic type parameter such as the T in your definition of Action<T>. If it's a specific type (like string or Date), it won't behave distributively. Similarly, complex expressions involving a type parameter (such as {x: T}) also prevent distribution. I often refer to the latter idea, where you modify the "bare" type parameter T like {x: T}, as "clothing" the type parameter.


In your example, T extends undefined is distributive:

type Action<T> = T extends undefined ? ... : ...

To disable distribution, you can rephrase the check so that T is clothed but still behaves the same way (so the validation passes only if T extends

undefined</code). The simplest approach is to enclose both sides of the <code>extends
clause within one-element tuple types), making T become [T] and undefined turn into [undefined]:

type Action<T> = [T] extends [undefined] ? ... : ...

This adjustment makes your code work as intended:

function handleApiResponse<T>(apiResponse: ApiResponse<T>) {
  const a: Action<ApiResponse<T>> = { 
    type: "response",
    payload: apiResponse,
  } // okay
}

Note that TypeScript considers tuples and arrays to be covariant in their element types, meaning that

Array<X> extends Array<Y>
or [X] extends [Y] if and only if X extends Y. While technically risky (refer to this question for more details), this covariance proves to be highly practical.


A useful rule in such cases is to transform an accidentally distributive expression like TTT extends UUU ? VVV : WWW by enclosing it within brackets to make it non-distributive: [TTT] extends [UUU] ? VVV : WWW. This technique, mentioned towards the end of the documentation on distributive conditional types, provides a solution without requiring any additional syntax changes. Despite appearing somewhat unique due to the square brackets, it simply utilizes existing one-element tuple syntax effectively.

Playground link to code

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

Error message TS2304: Unable to locate the variable 'title' in vite (vue) + typescript environment

My current project uses Vite (Vue) along with Typescript. However, when I execute the command yarn build (vue-tsc --noEmit && vite build), I encounter a series of errors similar to this one: Error TS2304: Cannot find name 'title'. http ...

Unlocking the power of variables in Next.js inline sass styles

Is there a way to utilize SASS variables in inline styles? export default function (): JSX.Element { return ( <MainLayout title={title} robots={false}> <nav> <a href="href">Title</a> ...

Angular 6 - detecting clicks outside of a menu

Currently, I am working on implementing a click event to close my aside menu. I have already created an example using jQuery, but I want to achieve the same result without using jQuery and without direct access to the 'menu' variable. Can someon ...

Issue with TypeScript Functions and Virtual Mongoose Schema in Next.js version 13.5

I originally created a Model called user.js with the following code: import mongoose from "mongoose"; import crypto from "crypto"; const { ObjectId } = mongoose.Schema; const userSchema = new mongoose.Schema( { //Basic Data ...

Developing a TypeScript NodeJS module

I've been working on creating a Node module using TypeScript, and here is my progress so far: MysqlMapper.ts export class MysqlMapper{ private _config: Mysql.IConnectionConfig; private openConnection(): Mysql.IConnection{ ... } ...

After compilation, what happens to the AngularJS typescript files?

After utilizing AngularJS and TypeScript in Visual Studio 2015, I successfully developed a web application. Is there a way to include the .js files generated during compilation automatically into the project? Will I need to remove the .ts files bef ...

Update gulp configuration to integrate TypeScript into the build process

In the process of updating the build system for my Angular 1.5.8 application to support Typescript development, I encountered some challenges. After a complex experience with Grunt, I simplified the build process to only use Gulp and Browserify to generat ...

Tips for deleting the background color from Angular 2 templates using Webstorm

The template and style in my Angular 2 project in WebStorm have an unwanted 'green' background. How can I get rid of this color? https://i.sstatic.net/ZKbD9.png ...

Nextjs REACT integration for self-service registration through OKTA

Attempting to integrate the Okta SSR feature for user sign-up in my app has been challenging as I keep encountering this error message: {"errorCode":"E0000060","errorSummary":"Unsupported operation.","errorLink& ...

What is the best way to prevent jest.mock from being hoisted and only use it in a single jest unit test?

My goal is to create a mock import that will be used only in one specific jest unit test, but I am encountering some challenges. Below is the mock that I want to be restricted to just one test: jest.mock("@components/components-chat-dialog", () ...

Discovering the data types for node.js imports

When starting a node.js project with express, the code typically begins like this - import express = require('express') const app = express() If I want to pass the variable 'app' as a parameter in typescript, what would be the appropri ...

React-Bootstrap columns are not displaying in a side by side manner and are instead appearing on separate lines

I am currently integrating Bootstrap into my React project alongside Material UI components. Below is a sample of one of my components: import { styled } from "@mui/material/styles"; import Paper from "@mui/material/Paper"; import Cont ...

What are some ways to leverage a promise-returning callback function?

Here is a function that I have: export const paramsFactory = (params: paramsType) => { return ... } In a different component, the same function also contains await getPageInfo({ page: 1 }) after the return .... To make this work, I need to pass a cal ...

Angular 2 decorators grant access to private class members

Take a look at this piece of code: export class Character { constructor(private id: number, private name: string) {} } @Component({ selector: 'my-app', template: '<h1>{{title}}</h1><h2>{{character.name}} detai ...

The specified property cannot be found within the type 'JSX.IntrinsicElements'. TS2339

Out of the blue, my TypeScript is throwing an error every time I attempt to use header tags in my TSX files. The error message reads: Property 'h1' does not exist on type 'JSX.IntrinsicElements'. TS2339 It seems to accept all other ta ...

Regulation specifying a cap of 100.00 on decimal numbers entered into a text input field (Regex)

I have created a directive that restricts text input to only decimal numbers in the text field. Below is the code for the directive: import { HostListener, Directive, ElementRef } from '@angular/core'; @Directive({ exportAs: 'decimal ...

What is the standard approach for indicating the lack of a specific attribute?

Is there a standardized way to specify that a specific property must definitely NOT appear on an object? I have come up with a method like this: type NoValue<T extends { value?: never, [key: string]: unknown }> = T type Foo = NoValue<{}> // Thi ...

Please explain the purpose of the httponly ss-tok bearerToken cookie in ServiceStack Authentication

While I comprehend the importance of implementing an httponly flag in the Set-Cookie Response header to enhance security and prevent XSS attacks, there is one aspect that remains unclear to me. Specifically, I am unsure about the purpose of the "ss-tok" co ...

Having difficulty invoking the forEach function on an Array in TypeScript

I'm currently working with a class that contains an array of objects. export interface Filter { sf?: Array<{ key: string, value: string }>; } In my attempt to loop through and print out the value of each object inside the array using the forE ...

What is the most effective way to determine the data type of a variable?

My search skills may have failed me in finding the answer to this question, so any guidance towards relevant documentation would be appreciated! I am currently working on enabling strict type checking in an existing TypeScript project. One issue I'v ...