Eliminate the need for 'any' in TypeScript code by utilizing generics and partials to bind two parameters

I'm working with TypeScript and have the following code snippet:

type SportJournal = { type: 'S', sport: boolean, id: string}
type ArtJournal = { type: 'A', art: boolean, id: string}
type Journal = SportJournal | ArtJournal;
type JournalState<T extends Journal> = {state: string, journal: T}

const updateSportJournal = (journal: Partial<SportJournal>) => { console.log(journal)}
const updateArtJournal = (journal: Partial<ArtJournal>) => { console.log(journal)}

const updateJournalState = <T extends Journal>(journalState: JournalState<T>, journal: Partial<T>) => {
  if (journalState.journal.type === 'S') {
      updateSportJournal(journal as any);
  } else if (journalState.journal.type === 'A') {
      updateArtJournal(journal as any);
  }
}

I want to use the updateJournalState function for both SportJournal and ArtJournal. However, I encountered a compiler error when calling the updateSportJournal method without casting to any. The error message is:

Argument of type 'Partial<T>' is not assignable to parameter of type 'Partial<SportJournal>'.   
Types of property 'type' are incompatible.     
Type 'T["type"]' is not assignable to type '"S"'.       
Type '"S" | "A"' is not assignable to type '"S"'.         
Type '"A"' is not assignable to type '"S"'.

Is there a way to make this code type-safe without using any?

Answer №1

Utilizing control flow analysis to narrow the type of journal by inspecting the value of journalState.journal.type within the function updateJournalState() is an ineffective approach for two main reasons:

  • TypeScript does not adjust or re-restrict generic type parameters like T through control flow analysis. Although you can narrow a value of type T to a more specific type, T itself remains unchanged. Therefore, even if you narrow journalState, it will have no impact on T and consequently no effect on journal. There are ongoing discussions on GitHub, such as microsoft/TypeScript#33014, regarding potential improvements in this area, but as of now, no changes have been implemented.

  • Even if the function was not generic, the type of journalState would likely be something along the lines of

    JournalState<SportJournal> | JournalState<ArtJournal>
    , which does not qualify as a discriminated union. Although the journal.type subproperty could be viewed as a discriminator, TypeScript only supports discriminant properties at the top level of the object and does not delve into subproperties for discriminating unions. A recurring request has been made on microsoft/TypeScript#18758 to support nested discriminated unions, yet there have been no implementations thus far.

To address these challenges, alternative methods may need to be explored, albeit potentially leading to complex solutions.


Alternatively, I suggest enhancing the generality of the function and substituting control flow branching (if/else) with a unified generic lookup that adheres to compiler standards. This modification necessitates particular refactoring steps, as outlined in microsoft/TypeScript#47109. The concept involves initiating a fundamental key-value type structure:

interface JournalMap {
    S: { sport: boolean, id: string },
    A: { art: boolean, id: string }
}

and expressing intended actions using mapped types over this base type alongside generic indexes into said mapped types.

For instance, defining Journal can follow this format:

type Journal<K extends keyof JournalMap = keyof JournalMap> =
    { [P in K]: { type: P } & JournalMap[P] }[K]

This creates a distributive object type, ensuring that while Journal collectively forms a union, individual members like Journal<"S"> and Journal<"A"> become distinct entities. If preferred, aliases can be assigned to these individual components:

type SportJournal = Journal<"S">;
type ArtJournal = Journal<"A">;

A similar definition to the previous example can be applied to JournalState:

type JournalState<T extends Journal<any>> =
    { state: string, journal: T }

In lieu of conditional statements like if/else, an object containing updater functions must be created, enabling indexing with either "S" or "A":

const journalUpdaters: {
    [K in keyof JournalMap]: (journal: Partial<Journal<K>>) => void
} = {
    S: journal => console.log(journal, journal.sport),
    A: journal => console.log(journal, journal.art)
}

This explicit assignment of the mapped type ensures that the compiler understands the relationship between K being a generic type and the resulting function type, preventing unnecessary unions.

Finally, a generic function can be utilized:

const updateJournalState = <K extends keyof JournalMap>(
    journalState: JournalState<Journal<K>>, journal: Partial<Journal<K>>) => {
    journalUpdaters[journalState.journal.type](journal); // valid
}

This code segment compiles error-free. By inferring that journalState.journal.type corresponds to type K, the compiler recognizes that

journalUpdates[journalState.journal.type]
aligns with type
(journal: Partial<Journal<K>>) => void
. Consequently, given that journal pertains to type Partial<Journal<K>>, the function call is permitted.


The functionality can be validated from the caller's perspective:

const journalStateSport: JournalState<SportJournal> = {
    state: "A",
    journal: { type: "S", sport: true, id: "id" },
};

updateJournalState(journalStateSport, { id: "a", sport: false }); // permissible
updateJournalState(journalStateSport, { art: false }) // error!

const journalStateArt: JournalState<ArtJournal> = {
    state: "Z",
    journal: { type: "A", art: true, id: "xx" }
};
updateJournalState(journalStateArt, { art: false }); // acceptable
updateJournalState(journalStateArt, { id: "a", sport: false }); // error!

Upon testing, it confirms that the compiler approves correct calls and flags incorrect ones accordingly.

Link to playground for code demonstration

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

Create a single declaration in which you can assign values to multiple const variables

As a newcomer to react/JS, I have a question that may seem basic: I need multiple variables that are determined by a system variable. These variables should remain constant for each instance. Currently, my approach is functional but it feels incorrect to ...

Unraveling the mysteries of an undefined entity

When the variable response is undefined, attempting to retrieve its property status will result in an error: Error: Unable to access property 'status' of undefined const { response, response: { status }, request, config, } = error as A ...

What could be causing a compile error in my React and TypeScript application?

I recently downloaded an app (which works in the sandbox) from this link: https://codesandbox.io/s/wbkd-react-flow-forked-78hxw4 However, when I try to run it locally using: npm install followed by: npm start I encounter the following error message: T ...

I am looking to personalize a Material UI button within a class component using TypeScript in Material UI v4. Can you provide guidance on how to achieve this customization?

const styling = { base: { background: 'linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)', border: 0, borderRadius: 3, boxShadow: '0 3px 5px 2px rgba(255, 105, 135, .3)', color: 'white', height: 48, ...

What is the best way to incorporate a WYSIWYG Text Area into a TypeScript/Angular2/Bootstrap project?

Does anyone know of a WYSIWYG text editor for TypeScript that is free to use? I've been looking tirelessly but haven't found one that meets my needs. Any recommendations or links would be greatly appreciated. Thank you in advance! ...

There seems to be a contradiction in my code - I am returning a Promise but TypeScript is throwing an error saying that the

I currently have a function that retrieves a bot's inventory on the Frontend fetchBotInventory() { this.socket.emit('fetch bot inv'); this.socket.on('bot inv', (botInventory) => { return new Promise((resolve, re ...

My component fails to load using Angular Router even though the URL is correct

I have been experiencing an issue while trying to load my Angular component using the router. The component never appears on the screen and there are no error messages displayed. app-routing-module { path: '', redirectTo: '/home', ...

The assets path is the directory within the installed package that houses the main application files following the completion of a

I have a Vue.js UI component that is internally built using webpack. This reusable UI component library references its images as shown below: <img src="./assets/logo.png"/> <img src="./assets/edit-icon.svg"/>   <i ...

Is there a way to utilize an Event Emitter to invoke a function that produces a result, and pause until the answer is provided before continuing?

Looking for a way to emit an event from a child component that triggers a function in the parent component, but with a need to wait for a response before continuing. Child @Output() callParentFunction = new EventEmitter<any>(); ... this.callParen ...

Guide on changing the background image of an active thumbnail in an autosliding carousel

My query consists of three parts. Any assistance in solving this JS problem would be highly appreciated as I am learning and understanding JS through trial and error. I have designed a visually appealing travel landing page, , featuring a thumbnail carous ...

What is the best way to create two MUI accordions stacked on top of each other to each occupy 50% of the parent container, with only their contents scrolling

I am looking to create a layout with two React MUI Accordions stacked vertically in a div. Each accordion should expand independently, taking up the available space while leaving the other's label untouched. When both are expanded, they should collect ...

Asserting types for promises with more than one possible return value

Struggling with type assertions when dealing with multiple promise return types? Check out this simplified code snippet: interface SimpleResponseType { key1: string }; interface SimpleResponseType2 { property1: string property2: number }; inter ...

Attempting to render the application results in an error message stating: "Actions must be plain objects. Custom middleware should be used for asynchronous actions."

I am experiencing an issue while testing my vite + typescript + redux application to render the App component using vitest for testing. I am utilizing redux@toolkit and encountering a problem when trying to implement async thunk in the app component: Error ...

Your search parameter is not formatted correctly

I am currently working on filtering a collection based on different fields such as name by extracting the values from the URL parameters. For example: http://localhost:3000/patient?filter=name:jack I have implemented a method to retrieve and convert these ...

Can I access the component attributes in Vuetify using Typescript?

For example, within a v-data-table component, the headers object contains a specific structure in the API: https://i.stack.imgur.com/4m8WA.png Is there a way to access this headers type in Typescript for reusability? Or do I have to define my own interfac ...

Incoming information obtained via Websocket

Currently, I am working with Angular and attempting to retrieve data from the server using websockets. Despite successfully receiving the data from the server, I am faced with a challenge where instead of waiting for the server to send the data, it retur ...

Elementary component placed in a single line

Creating a text dropdown menu using the following code: import { Autocomplete, TextField } from '@mui/material' import React, { useState } from 'react' const options = [ 'Never', 'Every Minute', 'Every 2 ...

The Conundrum of Angular 5 Circular Dependencies

I've been working on a project that involves circular dependencies between its models. After reading through this StackOverflow post and its suggested solutions, I realized that my scenario might not fit into the category of mixed concerns often assoc ...

NextJS introduces a unique functionality to Typescript's non-null assertion behavior

As per the typescript definition, the use of the non-null assertion operator is not supposed to impact execution. However, I have encountered a scenario where it does. I have been struggling to replicate this issue in a simpler project. In my current proj ...

What methods are available for altering state in Server Actions in NextJS 13?

Struggling to Implement State Change in NextJS 13 Server Actions I have encountered a challenge with altering state within the execution of server actions in NextJS 13. The scenario involves an actions.ts file located at the root of the app directory. Cur ...