Differentiate between function and object types using an enum member

I'm currently experimenting with TypeScript to achieve narrowed types when dealing with index signatures and union types without explicitly discriminating them, such as using a switch case statement.

The issue arises in the code snippet below when attempting to call the doubleFn variable with the shape. Although at runtime the correct shape (circle) is returned and the doubleFn function for doubling the radius is inferred correctly, an error is thrown during compilation.

My question is, Is there a way to narrow the type of doubleFn so that it recognizes it as the corresponding pair to the provided shape?

Link to TypeScript playground with the same code

enum Shapes {
  Circle,
  Square,
}

interface ShapeProperties {
  [Shapes.Circle]: {
    radius: number;
  };
  [Shapes.Square]: {
    length: number;
  };
}

type FunctionsType = {
  [key in Shapes]: (a: ShapeProperties[key]) => ShapeProperties[key];
};

const doubleFunctions: FunctionsType = {
  [Shapes.Circle]: (circleProps: ShapeProperties[Shapes.Circle]) => ({
    radius: circleProps.radius * 2,
  }),
  [Shapes.Square]: (squareProps: ShapeProperties[Shapes.Square]) => ({
    length: squareProps.length * 2,
  }),
};

interface Circle {
  type: Shapes.Circle;
  props: ShapeProperties[Shapes.Circle];
}

interface Square {
  type: Shapes.Square;
  props: ShapeProperties[Shapes.Square];
}

type Shape = Circle | Square;

function getShape(): Shape {
  return { type: Shapes.Circle, props: { radius: 5 } };
}

const shape = getShape();
const doubleFn = doubleFunctions[shape.type];

doubleFn(shape.props);

Answer №1

The issue at hand is closely associated with what I've referred to as "correlated records" (check out microsoft/TypeScript#30581); the compiler struggles to keep track of correlations between different union-typed values, leading to errors in scenarios like this one.

In this specific instance, we have doubleFun and shape.props, both seen by the compiler as being union-typed:

const doubleFn = doubleFunctions[shape.type];
/* const doubleFn: 
  ((a: {radius: number;}) => {radius: number;}) | 
  ((a: {length: number;}) => {length: number;}) 
*/

const props = shape.props;
/* const props: {radius: number;} | {length: number;} */

While these types are not incorrect per se, they lack the necessary information for the compiler to confirm that calling doubleFn(props) is safe:

doubleFn(props); // error!

The issue arises from the fact that perhaps doubleFn unexpectedly turns into a square-processing-function while props becomes circle-properties, or vice versa. Such mismatch would lead to runtime errors since the correlation between doubleFn and props is not reflected in the type system.


As of now, TypeScript does not offer an ideal solution to address this problem. One approach is to use switch/case statements or other conditional logic to trigger control flow analysis by the compiler to ascertain safety in each case. However, this can be redundant.

Another immediate solution is to resort to a type assertion where you personally guarantee the type safety of your code. This method carries risks as it puts the responsibility on you to verify safety. If using type assertions, confine them to a small portion of code that gets reused elsewhere:

const toShapeFunction = (f: FunctionsType) => <S extends Shape>(s: S) =>
  (f[s.type] as (s: S['props']) => S['props'])(s.props);

The function toShapeFunction() converts a parameter of type FunctionsType to a function that handles any Shape and produces the correct properties output type. The type assertion informs the compiler: "f[s.type] will accept a value of type s.props and produce an output of the same type," which compiles without error. Subsequently, you can safely utilize toShapeFunction():

const doubleFunction = toShapeFunction(doubleFunctions);

const newSh = doubleFunction(shape); // valid

const ci: Circle = {
  type: Shapes.Circle,
  props: { radius: 10 }
}
const newCi = doubleFunction(ci);
console.log(newCi.radius); // 20

const sq: Square = {
  type: Shapes.Square,
  props: { length: 35 }
}
const newSq = doubleFunction(sq);
console.log(newSq.length); // 70

All seems well.


This sums up the current scenario. At one point, I proposed ways to simplify handling correlated values (view microsoft/TypeScript#25051) but the idea did not gain significant traction. For now, consider refactoring your code so that the required type assertions are manageable.

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

Achieving checkbox values in Typescript: A guide

I need help with capturing the values of checked checkboxes and storing them in a string to use in my API. I want to retrieve the value if a checkbox is unchecked. <div *ngFor="let x of groupesTable"> <input type="checkbox" [(ngModel)] ...

Exploring the incorporation of an inclusive switch statement within a Redux reducer utilizing Typescript. Strategies for managing Redux's internal @@redux actions

After conducting extensive research, I have yet to discover a definitive answer to this query. There is a question posted on Stack Overflow that provides guidance on how to implement a thorough switch statement: How can I ensure my switch block covers al ...

"Error: The React TypeScript variable has been declared but remains

Seeking guidance on ReactTS. I'm puzzled by the undefined value of the variable content. Could someone shed light on why this is happening and how to assign a value to it for passing to <App />? The issue persists in both the index.tsx file and ...

In Angular, use the ngFor directive to iterate through items in a collection and add a character to each item except

Currently, I am iterating through my data list and displaying it in the view using spans: <span *ngFor="let d of myData"> {{d.name}}, </span> As shown above, I am adding a comma ',' at the end of each item to ensure a coherent displ ...

Oops! An error occurred: Uncaught promise in TypeError - Unable to map property 'map' as it is undefined

Encountering an error specifically when attempting to return a value from the catch block. Wondering if there is a mistake in the approach. Why is it not possible to return an observable from catch? .ts getMyTopic() { return this.topicSer.getMyTopi ...

Creating specific union types for a bespoke React hook

There are 4 objects with both similar and different keys. The union of these objects is used for database operations as follows -> type Objects = Food | Diary | Plan | Recipe ; A Custom Pagination Hook function usePaginate (key: string, options: Option ...

How to send variables to a function when a value changes in TypeScript and Angular

As someone who is new to Angular and HTML, I am seeking assistance with the following code snippet: <mat-form-field> <mat-select (valueChange)="changeStatus(list.name, card.name)"> <mat-option *ngFor="let i of lists"> {{i.name}} ...

Creating various import patterns and enhancing Intellisense with Typescript coding

I've been facing challenges while updating some JavaScript modules to TypeScript while maintaining compatibility with older projects. These older projects utilize the commonjs pattern const fn = require('mod');, which I still need to accommo ...

Error: Name 'AudioDecoder' could not be located

In my current project using React and TypeScript with Visual Studio Code 1.69.2 and Node 16.15.1, I encountered an issue. I am attempting to create a new AudioDecoder object, but I keep getting an error message stating "Cannot find name 'AudioDecoder ...

Tips for setting up a proxy with an enum

I am facing an issue with setting up a Proxy for an enum. Specifically, I have an enum where I want to assign a value to this.status using a Proxy. However, despite my expectations, the output "I have been set" does not appear in the console. Can anyone ex ...

Is there a way to reset useQuery cache from a different component?

I am facing an issue with my parent component attempting to invalidate the query cache of a child component: const Child = () => { const { data } = useQuery('queryKey', () => fetch('something')) return <Text>{data}& ...

Leveraging the power of React's callback ref in conjunction with a

I'm currently working on updating our Checkbox react component to support the indeterminate state while also making sure it properly forwards refs. The existing checkbox component already uses a callback ref internally to handle the indeterminate prop ...

Customizing MUI Themes with TypeScript: How do I inform TypeScript that the theme is provided by the provider?

Below is a modified demo code snippet extracted from Material UI documentation: function ThemeUsage() { const theme = { palette: { primary: { main: "#000", }, }, } as const; type DefaultThemeType = { theme: type ...

KeysOfType: Identifying the precise data type of each property

I came across a solution called KeysOfType on a post at : type KeysOfType<T, TProp> = { [P in keyof T]: T[P] extends TProp? P : never }[keyof T]; Here are the interfaces being used: interface SomeInterface { a: number; b: string; } interface A ...

`Is there a way to resolve the getStaticProps type issue in Next.js while utilizing InferGetStaticPropsType?`

I'm puzzled by an error that occurred with post props. The error message reads as follows: Property 'body' does not exist on type 'never'. https://i.stack.imgur.com/zYlxc.png Even when I specify the type, can there still be an er ...

The error occurred in Commands.ts for Cypress, stating that the argument '"login"' cannot be assigned to the parameter of type 'keyof Chainable<any>))`

Attempting to simplify repetitive actions by utilizing commands.ts, such as requesting email and password. However, upon trying to implement this, I encounter an error for the login (Argument of type '"login"' is not assignable to parameter of t ...

I'm having trouble with Angular pipes in certain areas... but I must say, Stackblitz is truly incredible

Encountering this issue: ERROR in src\app\shopping-cart-summary\shopping-cart-summary.component.html(15,42): : Property '$' does not exist on type 'ShoppingCartSummaryComponent'. The error disappears when I remove the c ...

Revamping HTML with AngularJS Directive: Enhancing the Layout with New Angular Attributes

I am currently facing an issue with the compiler not recognizing a new attribute I have added in a directive. In my Angular TypeScript code, I have the following setup: public class MyDirectiveScope: ng.IScope { foo: boolean; } public class MyDirecti ...

Incorporating node packages into your typescript projects

I have been exploring various discussions on this forum but I am still unable to make it work. My goal is to compile the following code in TypeScript. The code is sourced from a single JavaScript file, however, due to issues with module inclusion, I am foc ...

Dealing with GraphQL mutation errors without relying on the Apollo onError() function

When managing access to an API call server-side, I am throwing a 403 Forbidden error. While trying to catch the GraphQL error for a mutation, I experimented with various methods. (Method #1 successfully catches errors for useQuery()) const [m, { error }] ...