The TypeScript factory class anticipates an intersection

In my code, I have a class factory called pickSomething that generates a specific type based on a key provided from a ClassMap:

class A {
  keya = "a" as const;
}
class B {
  keyb = "b" as const;
}

type ClassMap = {
  a: A
  b: B
}


const pickSomething = <K extends keyof ClassMap>(key: K): ClassMap[K] => {
  switch (key) {
    case 'a':
      return new A(); // Error: A is not assignable to A & B
    case 'b':
      return new B(); // Error: B is not assignable to A & B
  }
  throw new Error();
}

// Externally, it functions correctly
const a = pickSomething('a').keya;
const b = pickSomething('b').keyb;

Externally, the method works fine (

const a = pickSomething('a').keya;
). However, internally, there are errors present in each return statement. TypeScript seems to expect ClassMap[K] to represent A & B. Is there a way to address this issue with better type annotations without relying on type assertions?

Answer №1

The issue at hand seems to stem from the fact that TypeScript does not narrow a type parameter extending a union through control flow analysis, as it typically would for specific union types. For more insights, refer to the discussion on microsoft/TypeScript#24085. Even though you've checked whether `key` is 'a' or 'b', and `key` is of type `K`, this check does not affect `K` itself. Since the compiler is unaware that `K` is anything more specific than 'a' or 'b', it cannot infer that `ClassMap[K]` could be broader than `A & B`. (Beginning with TypeScript 3.5, modifying a lookup property on a union of keys necessitates an intersection of properties; refer to microsoft/TypeScript/pull/30769.)

From a technical standpoint, it's valid for the compiler to resist narrowing in this scenario since there's nothing preventing the type parameter `K` from being specified as the complete union type 'a' or 'b', even after checking it:

pickSomething(Math.random() < 0.5 ? 'a' : 'b'); // K is 'a' | 'b'

Presently, there isn't a way to communicate to the compiler that you don't intend `K extends 'a' | 'b'`, but rather something like `K extends 'a'` or `K extends 'b'`; in essence, not a constraint to a union, but a union of constraints. If such expression were possible, checking `key` might narrow `K` itself and subsequently understand that, for instance, `ClassMap[K]` equals just `A` when `key` is `a`. Refer to microsoft/TypeScript#27808 and microsoft/TypeScript#33014 for related feature requests.

In lieu of these features, utilizing type assertions provides the most efficient means to compile your code with minimal modifications. Though not entirely type safe:

const pickSomething = <K extends keyof ClassMap>(key: K): ClassMap[K] => {
    switch (key) {
        case 'a':
            return new A() as A & B
        case 'b':
            return new B() as A & B
    }
    throw new Error();
}

the resultant JavaScript adheres to conventions, at the very least.


Another approach involves leveraging the compiler's ability to return a lookup property type `T[K]` by directly looking up a property of key type `K` on an object of type `T`. This could lead to refactoring your code as follows:

const pickSomething = <K extends keyof ClassMap>(key: K): ClassMap[K] => {
    return {
        a: new A(),
        b: new B()
    }[key];
}

If avoiding instantiation of `new A()` and `new B()` each time `pickSomething` is called is desired, consider using getters instead, ensuring only the necessary path is taken:

const pickSomething = <K extends keyof ClassMap>(key: K): ClassMap[K] => {
    return {
        get a() { return new A() },
        get b() { return new B() }
    }[key];
}

This compilation proceeds without errors and maintains type safety. However, the unconventional nature of the code raises questions about its value. For now, opting for a type assertion seems the most appropriate route. Hopefully, in due course, an improved solution will address the concerns raised in microsoft/TypeScript#24085, eliminating the need for assertions in your original code.


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

Monitoring inbound and outbound traffic in express middleware

I am in the process of incorporating a logger into my Express application. This logger needs to record both requests and responses (including status codes and body content) for each request made. My initial approach involves creating a middleware function ...

Using AngularJS to refresh information stored in an array

My objective is to create a basic application that allows users to adjust their availability on weekdays. The code is functioning correctly as it retrieves data from the select box. However, I encounter an issue when trying to update Monday's data and ...

Angular 7 throwing errors of undefined variables

I encountered an error message that says Property 'country' does not exist on type 'ViewComponent'. Did you mean 'country$'? I'm at a loss on how to resolve this issue. I understand that there is an undefined variable ca ...

Sorting information by class titles in AngularJS

Working with angularjs, take a look at my view code : <div style="width:70px;"> Show Online <input type="checkbox" ng-model="showonline" /> </div> <div ng-repeat="user in use ...

Develop an interactive React sidebar with changing elements

I'm in the process of developing a sidebar for a project, with the goal of making it similar to tools like Confluence. This means that we need the ability to rearrange documents and create subdirectory structures by simply moving the documents, with ...

What is the best way to depict object key replacements within a Typescript definition?

I currently have these types: type PossibleKeys = number | string | symbol; type ValueOf<T extends object> = T[keyof T]; type ReplaceKeys<T extends Record<PossibleKeys, any>, U extends Partial<Record<keyof T, PossibleKeys>> = ...

Encountered an issue when attempting to establish a connection with the REST

I am encountering an issue with connecting to a web service deployed on an Apache server using Jersey. The error message I receive is: Failed to load http://192.168.1.200:8199/CheckinnWeb/webapi/myresource/query: No 'Access-Control-Allow-Origin' ...

The browser does not store the cookie in the designated cookie tab within the application settings

Upon sending a login request, although the cookie is being received, it is not getting saved in the cookie tab under the application tab. Consequently, when other protected routes' APIs are accessed, the cookie is not available to req.cookie on the ba ...

Avoid the issue of markers clustering in Leaflet to have distinct markerClusterGroup icons without overlap

Is there a way to prevent two separate markerClusterGroups from overlapping in Leaflet? I have one with a custom Icon to differentiate between the two clusters, but for simplicity's sake, I've omitted that part in this example. var map = L.map(&q ...

Tips for displaying only the items that are currently visible in an AngularJS Dropdown

I am currently working with an AngularJs object that looks like this: $scope.data = {'options': [{ "id": 1, "text": "Option 1", "isHidden": 0 }, { "id": 2, "text": "Option 2", "isHidden": 1 }, { "id": 3, "text": "Option 3", "isHidden": 0 }]}; U ...

Invoke the submit function of a form located outside a Material UI dialog from within the dialog

I'm facing an issue with a nested form scenario. The <form/> inside another form is displayed as a Material UI dialog and rendered in a separate portal in the DOM. /* SPDX-FileCopyrightText: 2021 @koistya */ /* SPDX-License-Identifier: MIT */ i ...

The output of the http.get or http.request callback is only visible within the shell when using node.js

Just dipping my toes into node and aiming to avoid falling into the callback hell trap. I'm currently working with two files: routes.js fetch.js //routes.js var fetchController = require("../lib/mtl_fetcher/fetcher_controller"); var express = requir ...

Is it possible to establish role-based access permissions once logged in using Angular 6?

Upon logging in, the system should verify the admin type and redirect them to a specific component. For example, an HOD should access the admi dashboard, CICT should access admin2 dashboard, etc. Below is my mongoose schema: const mongoose = require(&apo ...

The loader fails to disappear even after the AJAX response has been received

I am currently working on a page that utilizes AJAX to display data. While the data is being fetched, I want to show a loading animation, and once the data has been retrieved, I want the loader to disappear. Below is a snippet of the code where I am attemp ...

The connection to Cordova.js is established, but the deviceready event is not

I've included cordova.js in my project, called app.initialize();, but for some reason deviceready event is not being triggered. Any ideas on what might be causing this issue? Here's the JavaScript code: var app = { initialize: function() { ...

Error: Unable to access 'push' property of null object in Next.js Link

Utilizing Vite to develop a reusable component has led to an error upon publishing and reusing it: TypeError: Cannot read properties of null (reading 'push') The code for the component is as follows: import React from "react"; import ...

Assigning a variable in jQuery to a PHP variable that has not been defined can halt the script

Here is the code snippet for a document ready function: $(document).ready(function(){ var id = <?php echo "" ?>; alert('boo'); if(id!=0){ $(' ...

Can you provide guidance on achieving a gradient effect throughout the mesh, similar to the one shown in the example?

Check out my code snippet on JSFiddle: https://jsfiddle.net/gentleman_goat66/o5wn3bpf/215/ https://i.sstatic.net/r8Vxh.png I'm trying to achieve the appearance of the red/green box with the border style of the purple box. The purple box was created ...

Steps for iterating through an array within an object

I currently have a JavaScript object with an array included: { id: 1, title: "Looping through arrays", tags: ["array", "forEach", "map"] } When trying to loop through the object, I am using the following ...

Spread operator in Typescript for complex nested collection types

I have implemented a Firestore database and defined a schema to organize my data: type FirestoreCollection<T> = { documentType: T; subcollections?: { [key: string]: FirestoreCollection<object>; }; }; type FirestoreSchema< T exte ...