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

Refine objects based on their properties without removing them from the dataset

I have a specific object structured as follows: var myObj: { 2:"None", 20:"A", 31:"A", 32:"A", Social:"B", Method:"None" } My goal is to retrieve the object without the properties 'Social' and 'Method'. Initia ...

Ways to show alternative data from a database in Laravel 8

I am working on a project where I need to display additional data based on the option selected from a dropdown menu populated from a database. Can anyone guide me on how to achieve this using javascript or jquery? https://i.stack.imgur.com/k3WLl.png Belo ...

I'm having trouble getting Tailwind CSS colors to work with my Next.js components. Any tips on how to apply background colors

https://i.stack.imgur.com/8RGS3.png https://i.stack.imgur.com/FRTOn.png Hey there! I'm currently experimenting with using Tailwind background colors in my Next.js project. However, I'm facing an issue where the background color is not being appl ...

Guide on accessing device details with Angular2 and Typescript

I'm working on populating a table with details using TypeScript, but I need some assistance. 1. Device Name 2. Device OS 3. Location 4. Browser 5. IsActive I'm looking for a solution to populate these fields from TypeScript. Can anyone lend me ...

SonarLint versus SonarTS: A Comparison of Code Quality Tools

I'm feeling pretty lost when it comes to understanding the difference between SonarLint and SonarTS. I've been using SonarLint in Visual Studio, but now my client wants me to switch to the SonarTS plugin. SonarLint is for analyzing overall pr ...

Extracting keys and values from a JSON string for analysis

My current code for the service now rest outbound call is functioning correctly. However, I am facing issues while trying to parse the JSON body in the second REST call and fetch values in the desired table format. try { var r = new sn_ws.RESTMessageV2 ...

Restricted inclusive collection utilizing embedded identifier

Attempting to segregate a discriminated union array into separate arrays of its union types has presented some challenges. I came across this particular question that provides generic discriminators. Unfortunately, the dataset I am working with doesn&apos ...

Delivering objects from controller in AngularJS

I'm currently working on a project where I need to retrieve objects from the controller. Here's a snippet of my code: score.component.js: angular.module('score').component('score',{ templateUrl : 'app/score/score.t ...

What is preventing WebRTC from re-establishing connection after being disconnected?

In my current React web application project, I am implementing a feature where users can engage in group calls using WebRTC through a NodeJS server running Socket.IO. The setup allows for seamless joining and leaving of the call, similar to platforms like ...

Converting an array of objects into a unified object and restructuring data types in TypeScript

A few days back, I posted a question regarding the transformation of an array of objects into a single object while preserving their types. Unfortunately, the simplified scenario I provided did not resolve my issue. In my situation, there are two classes: ...

Ways to automatically update property value in MongoDB once a particular date is reached

Is it feasible to schedule a future date for a document in MongoDB, such as 30 days from the current date, and then automatically update another property of the document when that future date arrives? For instance: creating an event document setting the ...

How to extract the value of a key from JSON using JavaScript

Need help with an API call to retrieve a list of subcategories? Here's an example of the JSON format: { "description": "Flower", "name": "Flower", "parent_id": "1" }, { "description": "Moon", "n ...

Troubleshooting Issues with https.request and Options in NodeJS Leading to Connection Resets

When working with NodeJS, I noticed that my code performs as expected when using https.get() to retrieve responses. However, the moment I make the switch to https.request() along with requestOptions, I encounter a connection reset error along with the foll ...

Limiting page entry with passport.js and express middleware

My server is set up to authenticate user login. I have successfully redirected users to the success page after authentication (and back to the login page if it fails). However, I am facing an issue with using my own express middleware to restrict access fo ...

I am looking to retrieve the body's background color using Regular Expressions

Trying to extract the background color from a CSS string: "body{ background-color: #dfdfdf; } " The color could also be in rgba(120,120,120) format. I am looking for a way to extract that color using regular expressions. I have tried using this patt ...

To set up the store in configureStore, you must provide one type argument for the 'MakeStore' generic type

Encountering an issue with MakeStore showing a Generic type error 'MakeStore' requires 1 type argument(s) .ts(2314) Here is the code from configureStore.ts: import { configureStore, EnhancedStore, getDefaultMiddleware, } from '@reduxj ...

Transforming vanilla JavaScript into jQuery

Is there a more efficient way to write this code using jQuery? It's functioning properly in Firefox but not in Safari or Chrome without any error messages, making it difficult for me to troubleshoot. Any suggestions or insights would be greatly appre ...

Unlock the power of JavaScript and jQuery by utilizing inner functions

Here's some JavaScript and jQuery code with an ajax request included. Can you solve the mystery of why success1() can be called, but not this.success2()? Any ideas on how to fix this issue? function myFunction() { this.url = "www.example.com/ajax ...

Assign a true or false value to every attribute of an object

Imagine we have a scenario with an interface like this: interface User { Id: number; FirstName: string; Lastname: string; Age: number; Type: string; } and a specific method for copying properties based on a flag. ...

The Google Books API initially displays only 10 results. To ensure that all results are shown, we can implement iteration to increment the startIndex until all results have

function bookSearch() { var search = document.getElementById('search').value document.getElementById('results').innerHTML = "" console.log(search) var startIndex = I have a requirement to continuously make Ajax calls ...