Is it possible for TypeScript to manage a dynamic return type that is not determined by a function parameter?

I am facing a challenge with dynamic type checking using a param type and seeking help to solve it. Even though it might be a difficult task, any assistance would be greatly appreciated!

Consider the following code:

class DefaultClass {
    defaultProp: number;
    constructor(num: number) {
        this.defaultProp = num;
    }
}

class MyClass extends DefaultClass {
    myProp ?: string;
    constructor(num: number, str ?: string) {
        super(num);
        this.myProp = str || "default value";
    }
    myMethod(str: string): string[] {
        return [str];
    }
}

interface Config {
    class: typeof DefaultClass;
}

const config: Config = {
    class: DefaultClass
}

function initConfig(classType: typeof DefaultClass) {
    config.class = classType;
}

// The parameter type is not relevant in this context
function myFunction(param: any): InstanceType<typeof config.class> {
    // Does something based on which class is used in config
    // Returns an instance of DefaultClass or MyClass depending on the current config
    return new config.class(1);
}

initConfig(MyClass);
const myInstance: MyClass = myFunction("something");
// ERROR: Property 'myMethod' is missing in type 'DefaultClass'
// But required in type 'MyClass'.

It's apparent that static type checking cannot change the return type dynamically based on changes made to the config object as it is not determined before runtime. Nonetheless, I am eager to explore if there is a possible solution.

Answer №1

When it comes to directly supporting your actions in TypeScript, there are some major challenges you'll encounter:

  • The type of variables needs to evolve over time.
  • These changes must be noticeable across different function scopes.

Absence of Arbitrary Type Mutations

In TypeScript, you can't simply change the type of an expression or variable at will. For example, you cannot do something like

let foo = {a: "hello"}; foo = {b: 123};
and expect the compiler to switch the type of foo from {a: string} to {b: number}. While you can define the type of foo as
let foo: {a?: string; b?: number}
, the compiler won't detect any changes occurring between assignments.

All that TypeScript offers is narrowing through control flow analysis. It allows you to make a variable's apparent type more specific using assignments, user-defined type guards, or assertion functions. However, these narrowing effects don't extend across function boundaries.

Lack of Type Narrowing Effects Across Functions

The inability for changes to propagate across function boundaries makes it challenging for TypeScript to assist in tracking varying types. Because only changes within the body of a function are visible to callers, implementing a general approach to persist type narrowing effects would severely impact compiler performance. This limitation has been discussed in detail in GitHub issue microsoft/TypeScript#9998.

This situation leaves you somewhat constrained.


Instead of struggling with these constraints, my suggestion is to align your code with what TypeScript comprehends well. It is best practice to maintain a variable's type consistent throughout its lifespan. Rather than modifying config by calling initConfig(), consider utilizing the parameters of initConfig to construct myFunction(). This approach involves creating a factory-like structure using class:

class Impl<T extends typeof DefaultClass = typeof DefaultClass> {
    class: T
    constructor(classType: T = DefaultClass as T) {
        this.class = classType;
    }
    myFunction(param: any) {
        return new this.class(1) as InstanceType<T>;
    }
}

const CurImpl = new Impl(MyClass);
const myInstance: MyClass = CurImpl.myFunction("something");

const DifferentImpl = new Impl();
const differentInstance: DefaultClass = DifferentImpl.myFunction("else");

With this setup, CurImpl remains aware of MyClass since it belongs to Impl<typeof MyClass> and retains a consistent type. If you wish to use an alternative class constructor, create a new instance of Impl<T> for another T.


In the above scenario, I utilized a generic parameter default to set typeof DefaultClass as the fallback when inferring T. In case the classType parameter is omitted, the compiler resorts to using DefaultClass. Although this method may lack full type safety, it assumes that users won't misuse T with calls like new Impl<typeof MyClass>(). To handle this effectively, a type assertion was employed to assure the compiler of DefaultClass's compatibility with type T if no classType is specified. There could be other potentially safer ways to achieve this (possibly eliminating the need for a class), but I digress from the main topic.

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

Developing a hidden entity that adopts an interface with multiple functions that have been overloaded

My TypeScript interface includes a single function named "send" with two different allowed signatures. export interface ConnectionContext { send(data: ConnectionData): void; send(data: ConnectionData, timeout: number): Promise<ConnectionData> ...

What is the best way to search through an array in TypeORM?

I am working on implementing user permissions management using TypeORM with PostgreSQL. The permissions are defined within the user entity in the following column: @Column({ type: 'text', array: true }) permissions: UserPermission[] = []; Th ...

Animating multiple elements in Angular 2 using a single function

Currently, I am delving into Angular and faced a challenge while attempting to create a toggle categories menu. Within my navbar component, I have an animation trigger set up as follows: trigger('slideCategory', [ state('opened&apo ...

Is it possible to display Angular Material Slider after the label?

Searching through the Angular Material docks, I came across the Sliders feature. By default, the slider is displayed first, followed by its label like this: https://i.sstatic.net/C5LDj.png However, my goal is to have the text 'Auto Approve?' sh ...

Managing time in an Angular application using Typescript

I am facing an issue with formatting the time obtained from an API in my FormArray. The time is received in the format: 14.21.00 My goal is to convert this time to the following format: 2:21 PM I have attempted to format it using Angular's DatePip ...

What is causing the error "has no properties in common with" in this wrapped styled-component?

When looking at the following code, Typescript is flagging an error on <HeaderInner>: [ts] Type '{ children: Element; }' has no properties in common with type 'IntrinsicAttributes & Pick & Partial>, "className"> & ...

MUI Chips serving as selectible tags with checkbox-like functionality

I have retrieved data from a JSON file containing information about different types of chips: [ { "id": "4", "name": "Caucasian" }, { "id": "5", "name": "Asian" }, ...

What is the mechanism behind the widening of object literal types in Typescript inference?

I've been reading up on how typescript broadens inferred types but I'm still not entirely clear about what's happening here: type Def = { 'T': { status: 5, data: {r: 'm'}}, } function route<S extends keyof Def> ...

Strategies for iterating over an array in React with TypeScript

I'm currently working on looping through an array to display its values. Here's the code I have: ineligiblePointsTableRows() { return this.state[PointsTableType.INELIGIBLE].contracts.map(contract => { return { applied: (&l ...

Implementing the "$store" property within Vue components

Click here for a guide on how to type the $store property. Unfortunately, I've been encountering issues with it. In my Vue 2 project created using vue-cliI, I included a vuex.d.ts file in ./src directory but the $store property in my components still ...

What steps can I take to troubleshoot and repair my accordion feature within an Angular project?

As a newcomer to Angular, I recently attempted to create an accordion component but encountered unexpected behavior. Here is the HTML code for my attempt: <div class="faq-item-container"> <h1 class="mt-1 mb-5"><strong>Frequently A ...

Guide on converting enums in Angular 6

I have been working on translating enums in Angular, but I'm facing a challenge. While I can read and display them in a dropdown menu, translating them is proving to be difficult. Here is an example of my code: export enum test { test1 = '1 - ...

When using this.$refs in Vue, be mindful that the object may be undefined

After switching to TypeScript, I encountered errors in some of my code related to: Object is possibly 'undefined' The version of TypeScript being used is 3.2.1 Below is the problematic code snippet: this.$refs[`stud-copy-${index}`][0].innerHTM ...

Sharing parameters between pages in Angular IonicPassing parameters between pages within an Angular Ionic application

Is there a way to pass parameters from the signup page to the signupotp page successfully? I am facing an issue where the OTP on the signupotp page is not being recognized because the parameters (email and mobile) are not getting passed properly. In my bac ...

Angular module with customizable configurations

I am interested in developing a customizable Angular 9 module with IVY and AOT enabled. In the latest version of Angular, IVY and AOT are automatically activated: npx @angular/<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="ed8 ...

Error: Typescript foreach loop encountering 'Expression yields void type'

Currently, I am working on setting up a cron job to monitor the completion of my tournaments and trigger some specific code upon completion. For reference, I came across this example: During deployment of my code, an error popped up as follows: ERROR: fu ...

What is the best way to transform typescript defined string types into an array of strings?

I'm attempting to extract all defined types from a variable in a constructor. export interface TestType { resultType?: 'NUMBER' | 'STRING' | 'DATE' | 'ENUM' | 'AMOUNT' ; } My expectation is to achie ...

The HTTP DELETE request encountered a TypeError, stating that error.json is not a valid function

Within my Angular application, there is a feature that involves a "delete button". When this button is clicked, a confirmation popup appears asking the user if they are certain they want to proceed with the deletion. If the user confirms by clicking ' ...

Error: setPosition function only accepts values of type LatLng or LatLngLiteral. The property 'lat' must be a numerical value in agm-core agm-overlay

Currently, I am utilizing Angular Maps powered by Google @agm-core and agm-overlay to implement custom markers. However, when using the (boundsChange) function on the agm-map component, an error occurs with the message "invalidValueError: setPosition: not ...

What causes observables stream to be cancelled or stopped by my HTTP request?

I am facing an issue with adding a new property called Blobs to each application in an array of applications. I need to make a separate HTTP request to fetch Blobs for each application using the switchMap operator. However, the problem is that only the las ...