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

Combining attributes of objects in an array

Is the title accurate for my task? I have an array structured like this: { "someValue": 1, "moreValue": 1, "parentArray": [ { "id": "2222", "array": [ { "type": "test", "id": "ID-100" }, { ...

Creating multiple copies of a form div in Angular using Typescript

I'm attempting to replicate a form using Angular, but I keep getting the error message "Object is possibly 'null'". HTML: <div class="form-container"> <form class="example"> <mat-form-field> ...

Error in AngularX TS: Trying to invoke a type that does not have a callable signature

Encountering an issue while working on a component, specifically during ng serve/build process. Please note that this error is different from any console errors, despite what some may think. The expected outcome is for the code to successfully build and ru ...

Changing the way in which text is selected and copied from a webpage with visible white space modifications

After working on developing an HTML parser and formatter, I have implemented a new feature that allows whitespace to be rendered visible by replacing spaces with middle dot (·) characters and adding arrows for tabs and newlines. You can find the current ...

Guidelines for creating a masterpage and details page layout in Angular 6

In my app.component.html file, I have the following code: <div style="text-align:center"> <h1> Welcome to {{ title }}! </h1> </div> <div> <p-menu [model]="items"></p-menu> </div> Below is the code ...

The static method in TypeScript is unable to locate the name "interface"

Is it possible to use an interface from a static method? I'm encountering an issue and could really use some help. I've been experimenting with TypeScript, testing out an interface: interface HelloWorldTS { name : string; } Here&ap ...

Adjusting table to include hashed passwords for migration

How can I convert a string password into a hash during migration? I've tried using the following code, but it seems the transaction completes after the selection: const users = await queryRunner.query('SELECT * FROM app_user;'); user ...

The error encountered during parsing the module for Next.js, Webpack, and TypeScript files (.ts, .tsx) was due to an unexpected token

Here is the content of my next.config.mjs file: export default { webpack: (config) => { // Find rules that includes current directory const rulesWithCurrentDir = config.module.rules.filter((rule) => rule.include && rule.include.incl ...

Can an L1 VPC (CfnVpc) be transformed into an L2 VPC (IVpc)?

Due to the significant limitations of the Vpc construct, our team had to make a switch in our code to utilize CfnVpc in order to avoid having to dismantle the VPC every time we add or remove subnets. This transition has brought about various challenges... ...

What causes a typed string literal to be recognized as a pure string in TypeScript?

Consider the TypeScript code below: type example = 'BOOLEAN' | 'MULITSELECT' | ''; interface IObjectExample { a: string, readonly b: example } const handleObj = (obj: IObjectExample) :void => { console.log(&ap ...

The entity is not validated by class-validator

Is it possible to utilize class-validator for validating columns in an Entity? The validation does not seem to work for columns: import { IsEmail } from 'class-validator'; @Entity() export class Admin extends BaseEntity { @Column({ unique: t ...

Angular Error: Attempting to access property 'then' of undefined object causes TypeError

I am struggling with implementing JHipster Auth-guard. I am facing an issue where canActivate is not triggered for a user without permission for a specific route. I have carefully examined my code, but the console shows that .then() is undefined at the sp ...

Error: unable to access cordova in Ionic 2Another wording for this error message could be:

While running my Ionic app using the command ionic serve -l, I encountered the following error message: Runtime Error Uncaught (in promise): cordova_not_available Stack Error: Uncaught (in promise): cordova_not_available at v (http://localhost:8100/bu ...

Utilizing a monorepo approach enables the inclusion of all *.d.ts files

Scenario: In our monorepo, we have 2 workspaces: foo and bar. foo contains the following files: src/file.ts src/@types/baz.d.ts The bar workspace is importing @monorepo/foo/src/file. While type-checking works for the foo workspace, it does not work fo ...

I am looking for a way to transfer data collected from an input form directly to my email address without the need to open a new window. As of now, I am utilizing angular

Is there a way to send this data to my email address? I need help implementing a method to achieve this. {Name: "John", phoneNumber: "12364597"} Name: "John" phoneNumber: "12364597" __proto__: Object ...

Angular 8's array verification feature lacks the ability to recognize preexisting elements

I've been trying to add and delete items in an array when a user selects or deselects the same item. However, it appears that either my array is not working properly or there is a bug in my code causing it to fail. <div class="grp-input"> ...

The code compilation of Typescript in a Dockerfile is not functioning as expected due to the error "Name 'process' cannot be found."

Here's the Dockerfile I'm working with: FROM node:latest WORKDIR /usr/src/app ENV NODE_ENV=production COPY package*.json . RUN npm install && npm i -g typescript COPY . . RUN tsc CMD [ "node", "./dist/index.js&qu ...

Internet Explorer is throwing unexpected routing errors in Angular 2

I have a spring-boot/angular 2 application that is working perfectly fine on Chrome, Safari, Opera, and Edge. However, when accessed through Internet Explorer, the app directly routes to the PageNotFound component. I have tried adding shims for IE in the i ...

To handle a 400 error in the server side of a NextJS application, we can detect when it

I'm facing a situation where I have set up a server-side route /auth/refresh to handle token refreshing. The process involves sending a Post request from the NextJS client side with the current token, which is then searched for on the server. If the t ...

How can I apply styling to Angular 2 component selector tags?

As I explore various Angular 2 frameworks, particularly Angular Material 2 and Ionic 2, I've noticed a difference in their component stylings. Some components have CSS directly applied to the tags, while others use classes for styling. For instance, w ...