Typescript - Guarantee Presence of Generic Property on Generic Type Through Clear Error Message

(Heads up for Typescript beginners)

I'm working on a reusable reducer function that takes a state and an action, but is designed to only accept state objects with a specific type at a particular key. This key is passed as a parameter to the function. If the passed state object doesn't have the specified key, I want the compiler to throw an error.

While I've managed to get this working, I'm not happy with how the compiler handles the error messaging. It doesn't directly point out that the expected property is missing, instead throwing other errors which I'll explain below.

Types

// FetchAction definition here...

export type FetchState = {
  status: FetchAction,
  timestamp: Date
} | null

export type LoginState = {
  token: string | null,
  fetching: FetchState
};

Base Reducer

const initialState: LoginState = {
  token: null,
  fetching: null
}

const loginReducer: Reducer<LoginState> = (state = initialState, action) => {
  //...other operations 
  return fetchingReducer(state, action, 'fetching');
}

Method 1

type FetchContainingState<S, K extends keyof S> = {
  [F in keyof S]: F extends K ? FetchState : S[F];
};

export const fetchingReducer = <S extends FetchContainingState<S, K>, K extends keyof S>(state: S, action: Action, key: K): S => {
  // implementation
}

This solution works well. When I make a mistake in the function call like so:

return fetchingReducer(state, action, 'fetchin');
(misspelling 'fetching'), I receive this error message:

Argument of type 'LoginState' is not assignable to parameter of type 'FetchContainingState'. Types of property 'token' are incompatible. Type 'string | null' is not assignable to type 'FetchState'. Type 'string' is not assignable to type 'FetchState'.

It's good that it catches the error, but it only mentions the property like "token", without explicitly indicating which property it expected but did not find.

Method 2

type EnsureFetchState<S, K extends keyof S> = S[K] extends FetchState ? S : never;

export const fetchingReducer = <S, K extends keyof S>(state: EnsureFetchState<S, K>, action: Action, key: K): S => {
   // implementation
}

This approach also works, and when I mistype the call as

return fetchingReducer(state, action, 'fetchin');
(instead of "fetching"), I get:

Argument of type 'LoginState' is not assignable to parameter of type 'never'.

Shorter response, but even less descriptive of the issue. It provides minimal information on what might be wrong with the arguments.

Conclusion

In Method 1, I used a Mapped Type, while in Method 2, I employed a Conditional Type to check if the values passed for state and key meet our criteria. However, both approaches generate error messages that don't clearly identify the actual problem.

As someone new to more advanced TypeScript types, there might be a simpler way or concept that I'm missing. Hopefully! But overall, my main question is: How can we perform this type checking on an object with a dynamic key more effectively, generating clearer error messages?

Answer №1

When explaining type constraints, it is best to be straightforward. If we have a type constraint for a type S that includes a property named K with type FetchState, there is no need to reference other properties:

export const fetchingReducer = <S extends {[k in K]: FetchState}, K extends string>(state: S, action: Action, key: K): S => {

This approach appears to generate the desired error messages, as shown by this example code (some type definitions were fabricated to ensure completeness):

export interface Action {
    a: string;
}
export interface FetchAction extends Action {
    f: string;
}
export type FetchState = {
  status: FetchAction,
  timestamp: Date
} | null
export type LoginState = {
  token: string | null,
  fetching: FetchState
};
const intialState: LoginState = {
  token: null,
  fetching: null
}
export type Reducer<S> = (s: S, a: Action) => S;

const loginReducer: Reducer<LoginState> = (state = intialState, action) => {

  fetchingReducer(state, action, 'fetching'); // Argument of type 'LoginState' 
        // is not assignable to parameter of type '{ fetching: FetchState; }'.
        // Property 'fetching' is missing in type 'LoginState'.

  fetchingReducer(state, action, 'token'); // Argument of type 'LoginState' is 
          // not assignable to parameter of type '{ token: FetchState; }'.
          // Types of property 'token' are incompatible.
          // Type 'string | null' is not assignable to type 'FetchState'.
          // Type 'string' is not assignable to type 'FetchState'.

  // OK
  return fetchingReducer(state, action, 'fetching')
}

export const fetchingReducer = <S extends {[k in K]: FetchState}, K extends string>(state: S, action: Action, key: K): S => {
  return {} as S;
}

Answer №2

Just like beauty, the readability of errors is subjective and depends on the perspective of the observer.

In my view, the most aesthetically pleasing error arises when a second overload is added with K being of type PropertyKey. This error occurs due to a conditional type, but specifically tied to the key parameter. The necessity of the additional overload stems from the issue where if K extends keyof S encounters an error on key, K will be inferred as keyof S instead of its actual value.

To address the error aspect, I employ a string literal type accompanied by a descriptive message. If there is a key named

"This porperty is not of type FetchState"
, then there might be a problem, though this scenario seems improbable.

type EnsureFetchState<S, K extends PropertyKey> = S extends Record<K, FetchState> ? {} : "This property is not of type FetchState";
export function fetchingReducer<S, K extends keyof S>(state: S, action: Action, key: K & EnsureFetchState<S, K>): S
export function fetchingReducer <S, K extends PropertyKey>(state: S, action: Action, key: K & EnsureFetchState<S, K>): S
export function fetchingReducer <S, K extends keyof S>(state: S, action: Action, key: K & EnsureFetchState<S, K>): S {
   // implementation
}

//Argument of type '"fetchin"' is not assignable to parameter of type '"fetchin" & "This property is not of type FetchState"'. 
return fetchingReducer(state, action, 'fetchin'); 

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

retrieve the state property from NavLink

I am encountering an issue with passing objects through components in my project. Specifically, I have a chat object within a component that defines a NavLink. When a user clicks on the ChatsElement, which is a link, the page navigates to the URL /friends/ ...

Is it possible to use Eclipse for debugging AngularJS and TypeScript code?

I recently dove into the world of TypEcs and am currently working on developing a webpage using Typescript and AngularJS that I'd like to debug in Eclipse. Is it feasible to debug a TypeScript and Angular page in Eclipse? If so, could you provide m ...

Typegoose's representation of modifying data

Recently, I delved into the world of NestJS and kickstarted a sample project. To integrate MongoDB seamlessly, I opted for Typegoose. A useful online tutorial () caught my eye, illustrating how to employ abstractions with base typegoose models. Hence, my ...

The primary route module is automatically loaded alongside all other modules

I have configured my lazy loaded Home module to have an empty path. However, the issue I am facing is that whenever I try to load different modules such as login using its URL like /new/auth, the home module also gets loaded along with it. const routes: R ...

What is the reason for TypeScript not throwing an error when an interface is not implemented correctly?

In my current scenario, I have a class that implements an interface. Surprisingly, the TypeScript compiler does not throw an error if the class fails to include the required method specified by the interface; instead, it executes with an error. Is there a ...

Tips for selecting specific types from a list using generic types in TypeScript

Can anyone assist me in creating a function that retrieves all instances of a specified type from a list of candidates, each of which is derived from a shared parent class? For example, I attempted the following code: class A { p ...

Using Rxjs to dynamically map values from an array with forkJoin

Greetings! I have a collection of Boolean observables and would like to apply a logical AND operation. Currently, I am passing static values 'a' and 'b', but I am unsure of the number of elements in the totalKeys array. import { forkJoi ...

Adjust the component suppliers based on the @input

If I were to implement a material datepicker with a selection strategy, I would refer to this example There are instances where the selection strategy should not be used. The challenge lies in setting the selection strategy conditionally when it is insta ...

Get the most recent two files from a set

I am currently facing a challenge in retrieving the first 2 documents from a collection in google cloud firestore. My current approach involves using the timestamp of the latest document and then calculating the time range to fetch the desired documents. l ...

Utilize a variable within a regular expression

Can the variable label be used inside a regex like this? const label = 'test' If I have the regex: { name: /test/i } Is it possible to use the variable label inside the regex, in the following way? { name: `/${label}/i` } What do you think? ...

Leveraging the power of react-hook-form in combination with the latest version 6 of @mui

When using MUI v5 date pickers, I utilized the following method to register the input with react-hook-form: <DatePicker ...date picker props renderInput={(params) => { return ( <TextField {...params} ...

Adding SVG to Component

I am attempting to embed an SVG element (retrieved using http.get()) into a 'icon' component. export class BgIcon { private svgSrc_: string; icon_: Icon; @Input('svg-src') set svgSrc(value: string) { this.svgSrc_ = value; ...

TypeScript Redux Thunk: Simplifying State Management

Seeking a deeper understanding of the ThunkDispatch function in TypeScript while working with Redux and thunk. Here is some code I found: // Example of using redux-thunk import { Middleware, Action, AnyAction } from "redux"; export interface ThunkDispatc ...

Setting an expiry date for Firestore documents

Is it feasible to set a future date and time in a firestore document and trigger a function when that deadline is reached? Let's say, today I create a document and specify a date for the published field to be set to false fifteen days later. Can this ...

Differences between RxJs Observable<string> and Observable<string[]>

I'm struggling to grasp the concept of RxJS Observables, even though I have utilized the observable pattern in various scenarios in my life. Below is a snippet of code that showcases my confusion: const observable: Observable<Response> = cr ...

Steps to resolve the Angular observable error

I am trying to remove the currently logged-in user using a filter method, but I encountered an error: Type 'Subscription' is missing the following properties from type 'Observable[]>': _isScalar, source, operator, lift, and 6 more ...

Automated tasks running on Firebase Cloud Functions with cron scheduling

There are two cloud functions in use: The first cloud function is used to set or update an existing scheduled job The second one is for canceling an existing scheduled job The management of scheduling jobs is done using import * as schedule from &ap ...

Switch up colors in Angular Material table rows

Is there a way to dynamically change row colors based on the date order in a table? In my mat table, I have a date field and I'm looking to highlight rows with the closest date in red and gradually transition to green for the furthest dates. ...

What is the function return type in a NextJS function?

In my project using NextJS 13, I've come across a layout.tsx file and found the following code snippet: export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html> <head /> <body&g ...

Jest's expect method fails to capture errors thrown by async/await functions

I'm currently experimenting with a TypeScript-Express application that utilizes MongoDB and Mongoose. To perform testing, I have integrated jest and mongo-memory-server into the project. While I have succeeded in testing the insertion of new documents ...