assigning the variable as an instance of the class returned by the function

Among the various cake classes, I have a function that returns an instance of one of them. My goal is to somehow change the variable to match the actual type of the returned value.

Visit TypeScript Playground

// Classes
class Cake{
    myName: string;
    base: string = "vanilla"
    frosting: string = "empty"
    constructor(myName: string) {
        this.myName = myName
    }
}

class Chocolate extends Cake{
    frosting: string = "chocolate"
    cut() {
        console.log("cut to 8 pieces!")
    }
}

class Strawberry extends Cake {
    frosting:string="strawberry"
    topping:string="coconut"
}

// Functions
function getCake(frosting:string, myName:string):Chocolate|Strawberry {
    const chocolateClass = new Chocolate(myName)
    const strawberryClass = new Strawberry(myName)
    const classes:any = { chocolateClass, strawberryClass }
    let returnedObj: Chocolate|Strawberry;
    Object.keys(classes).forEach((key) => {
        if (classes[key].frosting == frosting) {
            returnedObj = classes[key]
        }
    })
    return returnedObj
}

// Logic
const myCake: Strawberry | Chocolate = getCake("chocolate", "John")
console.log(myCake.frosting)  // able to access
console.log(myCake.myName)  // able to access
myCake.cut() // able to access, but with linting error and intellisense issue

I can access specific properties from the strawberry class,

View Console Result

However, the problem arises with linting and intellisense not recognizing the existing properties and methods. This led me to consider recasting the myCake variable based on the return type of the getCake() function. How can I achieve this?

I want to avoid creating separate functions for each cake type (getStrawberryCake()), especially when dealing with multiple cake classes.

Answer №1

If you want the compiler to automatically deduce the correct types for you instead of manually "casting" (or type asserting) multiple times in your code, you can achieve this by making some adjustments.

Firstly, the compiler may not grasp that the Chocolate class's frosting property will always be the exact string "chocolate" because you've defined it as type

string</code. To provide the compiler a better understanding of the relationship between <code>frosting
and the cake type, you can mark the properties as readonly and skip the type annotations. This helps the compiler infer that "chocolate" is a string literal type which can be used later to map cake frosting names to class types:

class Chocolate extends Cake {
  readonly frosting = "chocolate"
  cut() {
    console.log("cut into 8 pieces!")
  }
}

The same principle applies to the Strawberry class:

class Strawberry extends Cake {
  readonly frosting = "strawberry"
  topping: string = "coconut"
}

Next, we need to define a structure to map frosting to class types:

type PossibleCakes = Chocolate | Strawberry;
type CakeMap = { [K in PossibleCakes["frosting"]]: Extract<PossibleCakes, { frosting: K }> };
// type CakeMap = { chocolate: Chocolate; strawberry: Strawberry; }

The PossibleCakes type is simply the union you were using, and you can add more members to this union as needed. The CakeMap utilizes a mapped type with properties that utilize the Extract utility type to extract the right member from the PossibleCakes union. This results in the equivalent of

{chocolate: Chocolate; strawberry: Strawberry;}
.

Finally, we define the getCake() function. This should be a generic function so that when called with a specific frosting value, it returns a specific class, not the PossibleCakes union.

Here is the function:

function getCake<K extends keyof CakeMap>(frosting: K, myName: string): CakeMap[K] {
  return { chocolate: new Chocolate(myName), strawberry: new Strawberry(myName) }[frosting];
}

When you call getCake("chocolate", "John"), the return type will be inferred as Chocolate by the compiler. This utilizes the key-based property lookup of CakeMap. If K is "chocolate", then CakeMap[K] is Chocolate.

By employing object lookup in the function implementation, we enable the compiler to verify that the output will be a CakeMap[K]. This is more favorable for compiler inference compared to an iterative approach using Object.keys() and forEach(), which may require type assertions.

Test the function with the following code:

const myCake = getCake("chocolate", "John")
console.log(myCake.frosting)
console.log(myCake.myName)
myCake.cut() 

No errors should appear. The compiler recognizes that myCake is of type Chocolate, which means the cut() method is accessible. Hope this explanation helps, good luck!

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

Ionic: Fixed button located at the bottom of a specific ion-slide

I've been creating a series of slides with questions, and the final slide serves as a summary of the previously answered questions. I want to ensure that the submit button is always visible at the bottom of this last slide. However, I've encounte ...

Fixing the "Cannot find name" error by targeting ES6 in the tsconfig.json file

I recently started learning AngularJS through a tutorial. The code repository for the tutorial can be accessed at this link. However, upon running npm start using the exact code provided in the tutorial, I encountered the following error: Various TS2304 e ...

Can the inclusion of additional parameters compromise the type safety in TypeScript?

For demonstration purposes, let's consider this example: (playground) type F0 = (x?: string) => void type F1 = () => void type F2 = (x: number) => void const f0: F0 = (x) => console.log(x, typeof(x)) const f1: F1 = f0 const f2: F2 = f1 f ...

Using NavParams within a service component,

I'm facing a challenge in accessing NavParams within a provider, and simply importing NavParams is not solving the issue. Here's a brief overview of my application: users input their name and address, a pin is dropped on the map based on the add ...

Get rid of the TypeScript error in the specified function

I am currently working on implementing a "Clear" button for a select element that will reset the value to its default state. Here is a snippet of my code: const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => { onChange( ...

Removing fields when extending an interface in TypeScript

Attempting to extend the ISampleB interface and exclude certain values, like in the code snippet below. Not sure if there is an error in this implementation export interface ISampleA extends Omit<ISampleB, 'fieldA' | 'fieldB' | &apos ...

Angular app - static List mysteriously clears out upon refresh

My goal is to create a login page using Angular. I have an Angular component with HTML, CSS, and TypeScript files that manage this functionality. The HTML file contains two fields (Username and Password) and two buttons (Login and Register). When a user en ...

Leveraging bespoke components for data within Ionic 2

I have designed a custom component that functions like this: student.ts import { Component, Input } from '@angular/core'; @Component({ selector: 'student', templateUrl: 'student.html' }) export class StudentComponent { ...

Tips for utilizing ion-img within an Ionic 3 application?

I am currently developing an app using Ionic 3 that includes a large number of images spread across different pages. Initially, I used the default HTML image tag to display these images. However, this approach caused lag in the app's performance and a ...

Tips for creating a TypeScript function that is based on another function, but with certain template parameters fixed

How can I easily modify a function's template parameter in TypeScript? const selectFromObj = <T, S>(obj: T, selector: (obj: T) => S): S => selector(obj) // some function from external library type SpecificType = {a: string, b: number} co ...

Error message: Issue with AWS Serverless Lambda and Angular - TypeError: express function not defined

I'm encountering an issue when trying to deploy my application from localhost:4200 to AWS serverless Lambda. The error in cloudwatch logs is causing a 500 {"message": "Internal server error"} response when I access the URL. My understanding of AWS is ...

Why does WebStorm fail to recognize bigint type when using TSC 3.4.x?

Currently, I am working on the models section of my application and considering switching from using number to bigint for id types. However, despite knowing that this is supported from TSC 3.2.x, WebStorm is indicating an error with Unresolved type bigint. ...

Creating a Typescript project that compiles to UMD format, however, encountering the challenge of

I am trying to convert my index.ts file into a UMD index.js so that I can use it with a <script> tag. Here is the TypeScript configuration I am using: { "compilerOptions": { "outDir": "dist", "declaration& ...

Typescript error: The property "Authorization" is not found in the type HeadersInit

As I utilize the npm module node-fetch, I have a helper function specifically designed to facilitate authorized requests to a third-party service. This function essentially acts as middleware by incorporating the Authorization header. async function makeAu ...

Sending an object between two components without a direct parent-child connection

Hello, I have a question similar to the one asked on Stack Overflow regarding passing a value from one Angular 2 component to another without a parent-child relationship. In my scenario, Component1 subscribes to a service that makes a GET request to the ...

mobx: invoking a class method upon data alteration

Is it possible to utilize the Mobx library in order to trigger a class method whenever data changes? For instance, when MyObject assigns a value of 10 to container['item'], can we have the myaction method invoked? class MyElement extends Compone ...

Mocking multiple services and their constructors in an Angular 2 TypeScript Jasmine test component

I've got this login component code snippet that I need help testing in Angular. import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { FormBuilder, FormGroup, Validators } from '@ ...

updating firebase data using Angular

Currently, I am attempting to insert a new object into my Firebase database export class AppComponent { courses$: AngularFireList<any[]>; course$;ang author$ constructor(db: AngularFireDatabase) { this.courses$ = db.list('/courses ...

Guide to implement editable columns in Angular 4 with a click functionality

I have a table displaying records using ngFor, and I am looking to enable editing of a column upon clicking it. <tr *ngFor="let cd of descriptionCodes; let i = index"> <td><input type="checkbox"></td> <td> {{cd.code}} ...

Informing typescript that an argument is specifically an array when accepting both a single string and an array of strings

How can I inform TypeScript that the code is functionally valid? It keeps suggesting it could be a string, but I am unsure how that would happen. Is this a bug in my code or am I inputting something wrong? For example: const i18nInstance = { options ...