How can I restrict the return type of a generic method in TypeScript based on the argument type?

How can we constrain the return type of getStreamFor$(item: Item) based on the parameter type Item?

The desired outcome is:

  • When calling getStream$(Item.Car), the type of stream$ should be Observable<CarModel>
  • When calling getStream$(Item.Animal), the type of stream$ should be Observable<AnimalModel>

Currently, the type of stream$ is

Observable<CarModel | AnimalModel | TemperatureModel>
.

(refer to the 'HERE' comment in this StackBlitz)

enum Item {
  CAR = 'car',
  ANIMAL = 'animal',
  TEMPERATURE = 'temperature'
}

interface CarModel {
  make: string;
}

interface AnimalModel {
  breed: string;
}

interface TemperatureModel {
  tmp: number;
  scale: string;
}

class Channel {
  stream: {
    [prop in Item]: Observable<CarModel | AnimalModel | TemperatureModel>
  };

  constructor() {
    this.stream = {
      [Item.CAR]: of({ make: 'Ford' }) as Observable<CarModel>,
      [Item.ANIMAL]: of({ breed: 'Cat' }) as Observable<AnimalModel>,
      [Item.TEMPERATURE]: of({ tmp: 35, scale: 'Celsius' }) as Observable<TemperatureModel>
    };
  }

  getStreamFor$(item: Item): Observable<CarModel | AnimalModel | TemperatureModel> {
    return this.stream[item];
  }
}

const c = new Channel();
const stream$ = c.getStreamFor$(Item.CAR) // <--- HERE
  .pipe(
    map((value) => {
      value.make // <--- '.make' is marked as error by IDE
    }),
  )

Answer №1

When you annotate stream as

{ [K in Item]: Observable<CarModel | AnimalModel | TemperatureModel> }
, you essentially remove the specific mapping between each key from Item and each model for the compiler to remember. Similarly, by annotating the return type of getStreamFor$() as
Observable<CarModel | AnimalModel | TemperatureModel>
, that function can only return the wide union type causing problems. If you want the return type of getStreamFor$(item) to vary based on the type of item, consider making it a generic method or an overload.


To achieve the desired typing, it's recommended to let the types be inferred instead of using annotations. For stream, use a class field initializer instead of a declaration:

class Channel {
  stream = {
    [Item.CAR]: of({ make: 'Ford' }) as Observable<CarModel>,
    [Item.ANIMAL]: of({ breed: 'Cat' }) as Observable<AnimalModel>,
    [Item.TEMPERATURE]: of({ tmp: 35, scale: 'Celsius' }) as Observable<
      TemperatureModel
    >
  };

  constructor() {}

Then, simply make getStreamFor$() generic with type parameter I constrained to Item:

  getStreamFor$<I extends Item>(item: I) {
    return this.stream[item];
  }
}

This setup will work seamlessly:

const c = new Channel();
const stream$ = c.getStreamFor$(Item.CAR).pipe(
  map(value => {
    value.make; // now okay
  })
);

If content, stop here.


With IntelliSense enabled, you can view the inferred types of stream and getStreamFor$:

/* (property) Channel.stream: {
    car: Observable<CarModel>;
    animal: Observable<AnimalModel>;
    temperature: Observable<TemperatureModel>;
}

(method) Channel.getStreamFor$<I extends Item>(item: I): {
    car: Observable<CarModel>;
    animal: Observable<AnimalModel>;
    temperature: Observable<TemperatureModel>;
}[I] */

Note how the return type is defined in relation to the type of stream. To add annotations, assign names to stream's type and utilize it for annotation purposes:

interface ItemModelMap {
  [Item.CAR]: Observable<CarModel>;
  [Item.ANIMAL]: Observable<AnimalModel>;
  [Item.TEMPERATURE]: Observable<TemperatureModel>;
}

class Channel {
  stream: ItemModelMap;
  constructor() {
    this.stream = {
      [Item.CAR]: of({ make: 'Ford' }),
      [Item.ANIMAL]: of({ breed: 'Cat' }),
      [Item.TEMPERATURE]: of({ tmp: 35, scale: 'Celsius' })
    };
  }

  getStreamFor$<I extends Item>(item: I): ItemModelMap[I] {
    return this.stream[item];
  }
}  

Since stream is annotated as ItemModelMap, there's no need for as Observable<CarModel> in its initializer. You may move the initialization back within the constructor() method. The return type of getStreamFor$() can be expressed as ItemModelMap[I].

All functionalities remain intact while having robust type annotations.

Access code via Stackblitz link

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

Find out if a dynamically imported component has finished loading in Nextjs

Here is a simplified version of my current situation import React, { useState } from 'react'; import dynamic from 'next/dynamic'; const DynamicImportedComponent = dynamic(() => import('Foo/baz'), { ssr: false, loading ...

The 'target' property is not found on the type 'KeyboardEventHandler<HTMLInputElement>'

My Visual Studio Code is giving me an error in my onKeyUp function when I try to access the input target and retrieve its value. import React from 'react'; import styles from './styles.module.scss'; export function Step3() { ...

Include additional information beyond just the user's name, profile picture, and identification number in the NextAuth session

In my Next.js project, I have successfully integrated next-auth and now have access to a JWT token and session object: export const { signIn, signOut, auth } = NextAuth({ ...authConfig, providers: [ CredentialsProvider({ async authorize(crede ...

Tips for selecting specific types from a list using generic types in TypeScript

Can anyone assist me in creating a function that retrieves all instances of a specified type from a list of candidates, each of which is derived from a shared parent class? For example, I attempted the following code: class A { p ...

Please anticipate the reply from the AngularJS 2 API

I want to insert a token into a cookie, but the issue is that the cookie is created before receiving the API response. How can I make sure to wait for the response before creating the cookie? My Implementation: getLogin() { this._symfonyService.logi ...

A TypeScript default function that is nested within an interface

This is functioning correctly interface test{ imp():number; } However, attempting to implement a function within the interface may pose some challenges. interface test{ imp():number{ // do something if it is not overwritten } } I am ...

The ngAfterViewInit lifecycle hook does not get triggered when placed within ng-content

The ngAfterViewInit lifecycle hook isn't triggered for a Component that is transcluded into another component using <ng-content>, as shown below: <app-container [showContent]="showContentContainer"> <app-input></app-input> ...

Combine and transform multiple hierarchical JSONs into a new format

I'm facing a challenge where I need to merge two JSON objects and convert them into a different format using TypeScript in a React project. Initially, I tried implementing this with a recursive function as well as a reducer, but unfortunately, it didn ...

What is the best way to perform type checking for a basic generic function without resorting to using a cumbersome cast

Struggling with TypeScript and trying to understand a specific issue for the past few days. Here is a simplified version: type StrKeyStrVal = { [key: string]: string }; function setKeyVal<T extends StrKeyStrVal>(obj: T, key: keyof T, value: str ...

Deploying Angular to a shared drive can be done in a

My angular.json file contains the following line: "outputPath": "Y:\Sites\MySite", After running ng build, I encountered the error message: An unhandled exception occurred: ENOENT: no such file or directory, mkdir 'D:& ...

Vue3 project encountering issues with Typescript integration

When I created a new application using Vue CLI (Vue3, Babel, Typescript), I encountered an issue where the 'config' object on the main app object returned from the createApp function was not accessible. In VS Code, I could see the Typescript &ap ...

How can I effectively test the success of a form submission in next.js using jest for unit testing?

At the moment, I am in the process of developing a unit test for a registration form within my application. The main objective of this test is to ensure that the registration process can be executed successfully without actually saving any new data into th ...

Unit tests are failing to typecast the Angular HTTP GET response in an observable

I've been delving into learning about unit testing with Angular. One of the challenges I encountered involved a service method that utilizes http.get, pipes it into a map function, and returns a typed observable stream of BankAccountFull[]. Despite ...

TSLint Errors Update: The configuration provided cannot locate implementations for the following rules

After upgrading my tslint to version 4.0.2, I encountered numerous errors like the ones shown below: Could not find implementations for the following rules specified in the configuration: directive-selector-name component-selector-name directi ...

Navigating to view component in Angular2 Routing: Issue with router-link click event not working

I have set up my app.routes.ts file and imported all the necessary dependencies. Afterward, I connected all the routes to their respective Components as follows: import {ModuleWithProviders} from '@angular/core'; import {Routes, RouterModule} f ...

Issue with custom validator in Angular 6: setTimeout function not functioning as expected

Currently, I am in the process of following a tutorial to implement Asynchronous validation in Angular. The goal is to create a custom validator named shouldBeUnique that will be triggered after a 2-second delay. To achieve this, I have utilized the setTim ...

Using Svelte with Vite: Unable to import the Writable<T> interface for writable store in TypeScript

Within a Svelte project that was scaffolded using Vite, I am attempting to create a Svelte store in Typescript. However, I am encountering difficulties when trying to import the Writable<T> interface as shown in the code snippet below: import { Writa ...

Looking to retrieve the value of an input element within an ng-select in Angular 6?

Currently, I am working on a project where I aim to develop a customized feature in ng-select. This feature will enable the text entered in ng-select to be appended to the binding item and included as part of the multiselect function. If you want to see a ...

Optimal approach to configuring Spring Boot and Angular for seamless communication with Facebook Marketing API

Currently, I am working on a Spring Boot backend application and incorporating the Facebook marketing SDK. For the frontend, I am utilizing Angular 10. Whenever I create a new page or campaign, my goal is to send the corresponding object back to the fronte ...

Angular 5 offers the capability to use mat-slide-toggle to easily display and manipulate

I am experiencing an issue with displaying data in HTML using a mat-slide-toggle. The mat-slide-toggle works correctly, but the display does not reflect whether the value is 1 (checked) or 0 (unchecked). Can anyone provide some ideas on how to resolve this ...