Having an issue with return type in TypeScript when using an optional indexer

For this example, I aim to specify the return type of a function:


    type MakeNamespace<T> = T & { [index: string]: T }
    
    export interface INodeGroupProps<T = unknown[], State = {}> {
      data: T[];
      start: (data: T, index: number) =>  MakeNamespace<State>;
    }
    
    export interface NodesState {
      top: number;
      left: number;
      opacity: number;
    }
    
    export type Point = { x: number, y: number }
    
    const points: Point[] = [{ x: 0, y: 0 }, { x: 1, y: 2 }, { x: 2, y: 3 }]
    
    const Nodes: INodeGroupProps<Point, NodesState> = {
      data: points,
      keyAccessor: d => {
        return d.x
      },
      start: point => {
        return {
          top: point.y,
          left: point.x,
          opacity: 0,
        }
      }
    }
  

The potential return types for the start function are as follows:


    return {
      top: point.y,
      left: point.x,
      opacity: 0
    }
  

Or it could also be:


    return {
      namespace1: {
        top: point.y,
        left: point.x,
        opacity: 0
      },
      namespace2: {
        top: point.y,
        left: point.x,
        opacity: 0
      }
    }
  

An issue arises when TypeScript complains:

Property 'top' is incompatible with index signature

Modifying MakeNamespace to

type MakeNamespace<T> = T | { [index: string]: T }
solves the issue but doesn't handle cases like this:


    const Nodes: INodeGroupProps<Point, NodesState> = {
      data: points,
      start: point => {
        return {
          top: point.y,
          left: point.x,
          opacity: 0
          namespace1: {
            top: point.y,
            left: point.x,
            opacity: 0
          }
        }
      }
    }
  

In this case, where there is a mix of both.

Due to type widening in returns, the namespace key loses type safety by defaulting to the T section of the union.

A possible solution might involve making the indexer optional, though the approach is unclear at the moment.

You can experiment further on a playground.

Answer №1

If you have the ability to modify the code to make it more TypeScript-friendly, that would be my recommendation. One way to do this is by consolidating the extra namespaces into a single property with a specific name, which will simplify describing the type:

// defining an optional "namespaces" property as a collection of T
type MakeNamespace<T> = T & { namespaces?: { [k: string]: T | undefined } };

export interface INodeGroupProps<T, State> {
  start: (data: T, index: number) => MakeNamespace<State>;
}

export interface NodesState {
  top: number;
  left: number;
  opacity: number;
}

export type Point = { x: number, y: number }

// valid usage
const Nodes: INodeGroupProps<Point, NodesState> = {
  start: (point: Point) => {
    return {
      top: point.y,
      left: point.x,
      opacity: 0,
    }
  }
};

// also valid
const Nodes2: INodeGroupProps<Point, NodesState> = {
  start: (point: Point) => {
    return {
      top: point.y,
      left: point.x,
      opacity: 0,
      namespaces: {
        namespace1: {
          top: point.y,
          left: point.x,
          opacity: 0,
        }
      }
    }
  }
};

However, there is still an issue with widening the return type to

INodeGroupProps<Point, NodesState>
, causing the compiler to forget about properties like Nodes2.namespaces.namespace1:

const ret = Nodes2.start({ x: 1, y: 2 }, 0);
const oops = ret.namespaces.namespace1; // error! possibly undefined?
if (ret.namespaces && ret.namespaces.namespace1) {
  const ns = ret.namespaces.namespace1; // okay, ns is NodeState now
} else {
  throw new Error("why does the compiler think this can happen?!")
}

To address this, you can use helper functions to enforce matching annotations without widening the types:

const ensure = <T>() => <U extends T>(x: U) => x;

const ensureRightNodes = ensure<INodeGroupProps<Point, NodeState>>();


const Nodes = ensureRightNodes({
  start: (point: Point) => {
    return {
      top: point.y,
      left: point.x,
      opacity: 0,
    }
  }
});

const Nodes2 = ensureRightNodes({
  start: (point: Point) => {
    return {
      top: point.y,
      left: point.x,
      opacity: 0,
      namespaces: {
        namespace1: {
          top: point.y,
          left: point.x,
          opacity: 0,
          timing: { duration: 500 }
        }
      }
    }
  }
});

// Now Nodes and Nodes2 are narrowed:

const ret = Nodes2.start({ x: 1, y: 2 }); // Nodes2.start takes ONE param now
const ns = ret.namespaces.namespace1; // okay, ns is NodesState.

Both widening and narrowing have their advantages, so find the right balance for your requirements. Additionally, if needed, you can explore conditional types to maintain the original "T plus extra properties" definition with caution.

I hope this information proves helpful. Best of luck!

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

Using TypeScript 3.0 alongside React defaultProps

If I define a prop in the following way: interface Props { Layout?: LayoutComponent; } Then, if I set defaultProps on a ClassComponent: class MyComp extends React.Component<Props> { static defaultProps: Pick<Props, 'Layout'> = ...

Loop does not run as expected in TypeScript

Just dipping my toes into TypeScript here, so forgive me if I'm making a rookie mistake. Here's the code snippet I've got: const gCharData: any = {}; function buildChar() { const key = "Char1"; let currentChar = gCharData[k ...

Component html element in Angular not being updated by service

Within my Angular service, I have a property linked to a text field in a component's HTML. Oddly, when this property is updated by the service, the new value doesn't reflect in the HTML element unless the element is clicked on. I'm perplex ...

Select and activate a single input from multiple options in Angular

I have multiple input fields with corresponding buttons that enable the input when clicked. I would like the behavior of the buttons to work in such a way that when one button is clicked, only that specific input field is enabled while the others are disab ...

Trouble with Firebase cloud functions following the transition to TypeScript

Recently, I made an attempt to transition my firebase cloud functions from JavaScript to TypeScript and organized them into separate files. However, I encountered persistent errors while trying to deploy and serve the functions: Errors during serving: fu ...

Check the present value of Subject.asObservable() within an Angular service

I am working on developing a basic toggle feature within an Angular2 service. To accomplish this, I require the current value of a Subject that I am observing (shown below). import {Injectable} from 'angular2/core'; import {Subject} from ' ...

Issue with Hostlistener causing incorrect values for nativeelement.value and click events

I have been diving into Angular 4 and working on an autocomplete application. HTML: <form novalidate [formGroup] ="formG"> <input type="text" formGroupName="formCont" id="searText" class="searchBox"> </form> <div class="seracDrop ...

The subsequent code still running even with the implementation of async/await

I'm currently facing an issue with a function that needs to resolve a promise before moving on to the next lines of code. Here is what I expect: START promise resolved line1 line2 line3 etc ... However, the problem I'm encountering is that all t ...

Managing asynchronous variable assignment in Angular: tips and tricks

Within my Angular 6 application, there is a service where I declare a variable named "permittedPefs". This variable is asynchronously set within an httpClient.get call. @Injectable() export class myService implements OnInit { permittedPefs = [] ...

How to implement a toggle button in an Angular 2 application with TypeScript

Currently, I'm working with angular2 in conjunction with typescript. Does anyone know how to generate a toggle button using on - off?. ...

How do AppComponent and @Component relate to each other in AngularJs 2?

Recently, I came across the file app.component.ts in Example and found some interesting code. The link to the example is: here. Here's a snippet of the code: import { Component } from '@angular/core'; export class Hero { id: number; na ...

There is no universal best common type that can cover all return expressions

While implementing Collection2 in my angular2-meteor project, I noticed that the code snippets from the demo on GitHub always result in a warning message being displayed in the terminal: "No best common type exists among return expressions." Is there a ...

When trying to access a property in Typescript that may not exist on the object

Imagine having some data in JS like this example const obj = { // 'c' property should never be present a: 1, b: 2, } const keys = ['a', 'b', 'c'] // always contains 'a', 'b', or 'c' ...

What is the proper way to expand a TypeScript class?

I'm facing a dilemma with the code snippet below. The line m.removeChild(m.childNodes[0]) is causing an issue with the TypeScript compiler. I'm unsure if childNodes: BaseNode[]; is the correct approach in this scenario. class BaseNode { childNo ...

What causes an array to accumulate duplicate objects when they are added in a loop?

I am currently developing a calendar application using ExpressJS and TypeScript. Within this project, I have implemented a function that manages recurring events and returns an array of events for a specific month upon request. let response: TEventResponse ...

Ways to effectively test public functions in Typescript when using react-testing-library

I have come across the following issue in my project setup. Whenever I extend the httpService and use 'this.instance' in any service, an error occurs. On the other hand, if I use axios.get directly without any interceptors in my service files, i ...

Angular data binding between an input element and a span element

What is the best way to connect input texts with the innerHTML of a span in Angular6? Typescript file ... finance_fullname: string; ... Template file <input type="text" id="finance_fullname" [(ngModel)]="finance_fullname"> <span class="fullnam ...

How come JSON.parse is altering the data within nested arrays?

In my journey to master Angular 2, I decided to challenge myself by creating a Connect Four game using Angular CLI back when it was still utilizing SystemJS. Now, with the switch to the new Webpack-based CLI, I am encountering a peculiar issue... The fun ...

`Is there a way to verify existence and make changes to an object within a nested array using mongodb/mongoose?`

We are currently in the process of developing a REST API using node.js and typescript for an Inventory Management Web App. In our database, the Records>Stocks documents (nested arrays) are stored under the Branches collection. Records: This section sto ...

Tips for creating consecutive HTTP API requests in Angular 2

Is there a way to use RXJS in order to handle multiple API calls, where one call is dependent on another? ...