Uncover the value type of a key in TypeScript without using a discriminated union

I want to implement a type map that ensures the type of an object's value for a specific key is determined by the value of another key. For example:

type CurrencyValue = {
  code: string;
  value: number;
};

type FieldMap = {
  text: string;
  currency: CurrencyValue;
};

type FieldKind = keyof FieldMap;

type Field<T extends FieldType> = {
  kind: FieldKind;
  value: FieldMap[T];
};

In this scenario, if I create an object with the structure Field, and its kind is set to "text", then the expected type of value should be a string. If the kind is "currency", then the value should match the CurrencyValue type.

While I can almost achieve this functionality with the existing setup, I find myself needing to specify a parameter in the type:

// this works
const field: Field<"currency"> = {
  type: "currency",
  value: {
    code: "USD",
    value: 12,
  },
};

// this doesn't
// error: Generic type 'Field' requires 1 type argument(s).ts(2314)
const field: Field = {
  type: "currency",
  value: {
    code: "USD",
    value: 12,
  },
};

However, I wish to avoid having to explicitly define the type parameter in these cases. Is there a way for TypeScript to infer the type of value without explicit declaration?

One approach that has worked for me involves using discriminated unions:

type CurrencyField = {
    type: 'currency',
    value: CurrencyValue;
}

type TextField = {
    type: 'text',
    value: string;
}

type Field = CurrencyField | TextField;

const field: Field = {
    type: 'text',
    value: 'string'
}

However, creating multiple new types whenever a new version of a Field is introduced is not ideal. Therefore, it would be more convenient to simply update the mapping.

Thank you for your assistance!

Answer №1

To create the discriminated union, you don't have to manually write it out. You can achieve this with the following code:

type Field<T extends FieldKind = FieldKind> = T extends T ? {
  kind: T;
  value: FieldMap[T];
}: never;

Playground Link

This code leverages the distributive property of conditional types to generate a union of object types from a union of field names (such as

"currency"|"text"
), creating one object type for each constituent of the original union (
{ kind: "currency"; value: FieldMap["currency"]; } | { kind: "text"; value: FieldMap["text"]; }

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

Error in Typescript persists even after short-circuit evaluation is used

Kindly review the provided code sample type type1 = string[] | undefined; let variable1 : type1 = undefined; console.log(variable1 && variable1.length); Upon attempting to run this code in Typescript Playground, an error is generated stating Pro ...

TypeScript: empty JSON response

I am encountering an issue with the JSON data being blank in the code below. The class is defined as follows: export class Account { public amount: string; public name: string; constructor(amount: string, name: string) { this.amount = amount; t ...

Displaying search results in various Angular components

On my home page (homePageComponent), I have a search feature. When the user clicks on the search button, they are redirected to a different page called the search list page (searchListComponent). Within the searchListComponent, there is another component c ...

An unusual problem encountered while working with NextJS and NextAuth

In my NextJS authentication setup, I am using a custom token provider/service as outlined in this guide. The code structure is as follows: async function refreshAccessToken(authToken: AuthToken) { try { const tokenResponse = await AuthApi.refre ...

Angular testing with Jasmine and TypeScript

I've been attempting to create some Angular Controller tests for my App using TypeScript for a few days now, but haven't had any success. Let me start by saying that this is my first time writing tests in Jasmine. My issue is that I'm having ...

What is the proper way to include type annotation in a destructured object literal when using the rest operator?

When working with objects, I utilize the spread/rest operator to destructure an object literal. Is there a way to add type annotation specifically to the rest part? I attempted to accomplish this task, but encountered an error when running tsc. const { ...

Exploring the benefits of using TypeScript with Angular2 for HTTP

I have a locally stored "region.json" file containing the following data: { "regionId":1, "region":"CAN" }, { "regionId":2, "region":"CEN" } Additionally, I have an "enviroment-app.component.ts" file structured as follows : import {Component, ...

Guide on properly defining typed props in Next.js using TypeScript

Just diving into my first typescript project and finding myself in need of some basic assistance... My code seems to be running smoothly using npm run dev, but I encountered an error when trying to use npm run build. Error: Binding element 'allImageD ...

How can Enums be utilized as a key type for transmitting properties in Vue?

After stumbling upon a helpful method on Stack Overflow that demonstrated how to use an enum to define an object, I decided to implement this in my Vue project. export enum Options { randSize = 'randomSized', timer = 'showTimer', ...

What is the process for creating an additional username in the database?

As a beginner frontend trainee, I have been tasked with configuring my project on node-typescript-koa-rest. Despite my best efforts, I encountered an error. To set up the project, I added objection.js and knex.js to the existing repository and installed P ...

Problem with dynamic page routes in Next.js (and using TypeScript)

Hi everyone, I'm currently learning next.js and I'm facing an issue while trying to set up a route like **pages/perfil/[name]** The problem I'm encountering is that the data fetched from an API call for this page is based on an id, but I wa ...

Update all occurrences of a particular value to null within the Realtime Database using Firebase Cloud Functions

I need to update the values of a specific userID linked to multiple post keys in my database by setting the userID to null. The userIDs are associated with post keys located in the path: posts/ivies/userIDs in my database. Take a look at how the database i ...

Creating a flexible TypeScript function handler that accepts optional arguments depending on the function's name

I am facing a challenge with defining the function type for running helper functions that prepare database queries. Some of these functions have arguments, while others do not. TS Playground Link type PreparedQuery = { query: string; params?: string[] ...

Utilizing Typescript's compilerOptions.outDir for efficient compilation with external non-TS modules

I am encountering an issue with non-ts modules (text assets) not being transferred to the outDir as specified in tsconfig.json (or I might not be performing the task correctly). Here is a simple example to reproduce the issue: // /src/main.ts import text ...

What is the best way to restrict the maximum number of items stored in local storage?

I'm creating a GitHub search app using the GitHub API in Angular. I want to restrict the number of items that can be stored in local storage. If the number of stored elements exceeds 5, the "Add to Favorite" button should either stop working or disapp ...

Guide on invoking child components' functions from the parent component in Angular 6

Main Component import { Component } from '@angular/core'; import { DisplayComponent } from './display.component'; @Component({ selector: 'my-app', template: ` <button (click)="submit()">Call Child Com ...

Encountering an error with Mongoose's .pre('save') method in Typescript

Every time I attempt to use the hash password .pre hook, it refuses to save. userSchema.pre("save", async function (next) { let user = this as UserDocument; if (!user.isModified("password")) return next(); const salt = await bcry ...

Refresh Angular component upon navigation

I have set up routes for my module: const routes: Routes = [ { path: ":level1/:level2/:level3", component: CategoriesComponent }, { path: ":level1/:level2", component: CategoriesComponent}, { path: ":level1", component: ...

Discover the steps to update the rows, adjust the number of results, and manage pagination in angular-datatable with an observable as the primary data source

Incorporating angular-datatables into my Angular 7 application has been a challenge, especially when it comes to displaying search results efficiently. Within the request-creation-component, a search form is generated. Upon clicking the submit button, an ...

Tips for incorporating external AMD modules with Angular2 using WebPack

In my current project using Angular2, TypeScript, and WebPack, I am looking to incorporate the Esri ArcGIS JavaScript API. The API can be accessed from the following URL: By importing Map from 'esri/Map';, I aim to import the corresponding modul ...