Accurate TS declaration for combining fields into one mapping

I have a data structure called AccountDefinition which is structured like this:

something: {
  type: 'client',
  parameters: {
    foo: 3
  }
},
other: {
  type: 'user',
  parameters: {
    bar: 3
  }
},
...

The TypeScript declaration is functioning properly, but I am currently facing difficulties while trying to create a "generator" function (doThings) and struggling with how to correctly define its types. I am considering refactoring all these types as well.

export interface Spec {
  type: `${SpecType}`
  parameters: unknown
}

export interface UserSpec extends Spec {
  type: `${SpecType.USER}`
  parameters: UserSpecParameters
}

export interface ClientSpec extends Spec {
  type: `${SpecType.CLIENT}`
  parameters: ClientSpecParameters
}

export interface AccountDefinition {
  [k: string]: UserSpec | ClientSpec
}

export enum SpecType {
  USER = 'user',
  CLIENT = 'client'
}

export type SpecParametersMap = {
  user: {
    bar?: number
  }
  client: ClientSpecParameters
}

export interface UserSpecParameters {
  bar?: number
}

export interface ClientSpecParameters {
  foo: number
}

export const doThing = <T extends SpecType>( // Preferably not generic if inference works from type
  type: T,
  parameters: SpecParametersMap[T]
): void => {
  const account: AccountDefinition = {
    // Example
    foo: {
      type: 'client',
      parameters: {
        foo: 3
      }
    },
    // TS Error:
    // Type '{ parameters: SpecParametersMap[T]; type: T; }' is not assignable to type 'UserSpec | ClientSpec'.
    // Type '{ parameters: SpecParametersMap[T]; type: T; }' is not assignable to type 'ClientSpec'.
    // Types of property 'type' are incompatible.
    // Type 'T' is not assignable to type '"client"'.ts(2322)
    data: {
      parameters,
      type
    }
  }
}

doThing(SpecType.CLIENT, { foo: 4 })

Test it on the Playground here.

Answer №1

The issue at hand lies within the arguments. TypeScript does not treat them as a unified data structure.

To resolve this, you need to combine your arguments into a single data structure.

export enum SpecType {
  USER = 'user',
  CLIENT = 'client'
}

export interface Spec {
  type: `${SpecType}`
  parameters: unknown
}

export interface ClientSpecParameters {
  foo: number
}

export interface UserSpecParameters {
  bar?: number
}

export interface UserSpec extends Spec {
  type: `${SpecType.USER}`
  parameters: UserSpecParameters
}

export interface ClientSpec extends Spec {
  type: `${SpecType.CLIENT}`
  parameters: ClientSpecParameters
}

type AllowedValues = UserSpec | ClientSpec;

export interface AccountDefinition {
  [k: string]: AllowedValues
}


export const doThing = (data: AllowedValues): void => {
  const account: AccountDefinition = {
    foo: {
      type: 'client',
      parameters: {
        foo: 3
      }
    },
    data
  }
}

doThing({ type: SpecType.CLIENT, parameters: { foo: 4 } }) // okay
doThing({ type: SpecType.USER, parameters: { bar: 42 } }) // okay


You can explore this alternative approach, although note that there is no elegant destructuring involved

Why is it not possible to utilize the generic param to access both arguments?

In reality, you can achieve this using multiple methods. Here's one example:

export const doThing = <T extends SpecType>(data: T extends SpecType.CLIENT ? ClientSpec : UserSpec): void => {
    const account: AccountDefinition = {
        foo: {
            type: 'client',
            parameters: {
                foo: 3
            }
        },
        data
    }
}

doThing({ type: SpecType.CLIENT, parameters: { foo: 4 } }) // okay
doThing({ type: SpecType.USER, parameters: { bar: 42 } }) // okay

doThing({ type: SpecType.USER, parameters: { foo: 4 } }) // expected error
doThing({ type: SpecType.CLIENT, parameters: { bar: 42 } }) // expected error

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

A step-by-step guide on incorporating MarkerClusterer into a google-map-react component

I am looking to integrate MarkerClusterer into my Google Map using a library or component. Here is a snippet of my current code. Can anyone provide guidance on how I can achieve this with the google-map-react library? Thank you. const handleApiLoaded = ({ ...

Efficiently Updating Property Values in Objects Using TypeScript and Loops

I have been looking into how to iterate or loop through a code snippet, and while I managed to do that, I am facing an issue where I cannot change the value of a property. Here is the snippet of code: export interface BaseOnTotalPaidFields { groupName ...

Unidentified object type detected - Material UI

This snippet of code was taken directly from the React Material UI website <Select id="selectedSubusecases" multiple value={stepsState.campaignOverviewStep.selectedSubUsecases} ...

Translate array into object with correct data types (type-specific method)

Welcome In our project, we have implemented attributes support where each attribute acts as a class. These attributes include information on type, optionality, and name. Instead of creating an interface for every entity, my goal is to automate this proces ...

The http post request is functioning properly in postman, but it is not working within the ionic app

I am currently developing an app in ionic v3 within visual studio (Tools for Apache Cordova). On one of the screens in my app, I gather user information and send it to an API. However, I'm encountering an issue with the HTTP POST request that I'm ...

In TypeScript, leveraging the spread operator to determine the largest value in an array containing nullable numbers

Having an array of nullable numbers presented in the following way: let myArray : Array<number | null> = [1,2,null,4,null,5]; let maximumOfMyArray = Math.max(...myArray); // Type null is not assignable to type number I am content with JavaScript tre ...

Is it possible to capture and generate an AxiosPromise inside a function?

I am looking to make a change in a function that currently returns an AxiosPromise. Here is the existing code: example(){ return api.get(url); } The api.get call returns an object of type AxiosPromise<any>. I would like to modify this function so ...

A more efficient method for incorporating types into props within a functional component in a TypeScript-enabled NextJS project

When dealing with multiple props, I am looking for a way to add types. For example: export default function Posts({ source, frontMatter }) { ... } I have discovered one method which involves creating a wrapper type first and then defining the parameter ty ...

Why is it that TypeScript struggles to maintain accurate type information within array functions such as map or find?

Within the if block in this scenario, the p property obtains the type of an object. However, inside the arrow function, it can be either an object or undefined. const o: { p?: { sp?: string } } = { p: {} } if (o.p) { const b = ['a'].map(x => ...

Is it Feasible to Use Component Interface in Angular 5/6?

My main goal is to create a component that can wrap around MatStepper and accept 2..n steps, each with their own components. In other languages, I would typically create an interface with common behavior, implement it in different components, and then use ...

Utilizing the MapToIterable Angular Pipe with TypeScript

Exploring the implementation of a pipe in Angular. Discovered that ngFor doesn't work with maps, prompting further research to find a solution. It seems that future features may address this issue, but for now, utilizing a mapToIterable pipe is the re ...

What is the easiest way to compile a single .ts file in my src folder? I can achieve this by configuring the tsconfig.js file and running the yarn

{ "compilerOptions": { "target": "es5", "lib": [ "dom", "dom.iterable", "esnext" ], "allowJs": true, "skipLibCheck": true, ...

A guide to simulating ngControl in a Custom Form Control for effective unit testing in Angular

I need some guidance on creating unit tests for a Custom Form Control in Angular 9. The issue arises with this line of code: constructor(@Self() private ngControl: NgControl), which triggers an error: Error: NodeInjector: NOT_FOUND [NgControl]. It seems th ...

Ways to implement distinct values for model and input field in Angular 5

I'm currently working on an Angular 5 application and I have a requirement to format an input field with thousand separators (spaces). However, the model I am using only allows numbers without spaces. Since my application is already fully developed, ...

Designing functional components in React with personalized properties utilizing TypeScript and Material-UI

Looking for help on composing MyCustomButton with Button in Material-ui import React from "react"; import { Button, ButtonProps } from "@material-ui/core"; interface MyButtonProps { 'aria-label': string, // Adding aria-label as a required pro ...

Custom Mui table sizes - personalized theme

By implementing custom sizes for the Table component in Material UI, I have extended the Table size prop with the following declaration: declare module '@mui/material' { interface TablePropsSizeOverrides { relaxed: true large: true } ...

Announce enhancements to a Typescript library

Utilizing Sequency's extendSequence() feature to enhance all Sequence instances with a custom method: import Sequence, { extendSequence, isSequence } from 'sequency' import equal from '@wry/equality' class SequencyExtensions { e ...

Unable to locate the specified nested module during the import process

Imagine a scenario where we have two packages, namely package1 and package2. When package2 attempts to import the module from package1, an error is thrown stating that the module is not found. The import statement in question looks like this: import { ... ...

Angular's custom validator consistently returns a null value

I need help with validating the uniqueness of a username field in a form where an administrator can create a new user. I have implemented a uniqueUserNameValidator function for this purpose, but it always returns null. I suspect that the issue lies in the ...

Enhancing MUI themes by incorporating module augmentation for multiple typings

I am looking to create a repository with two directories, each using MUI and TypeScript. Both directories will have their own theme defined in one ThemeProvider per root. In the main index.tsx file in the root directory, I want to specify which component t ...