Mastering TypeScript: Navigating through generic types and harnessing the power of the keyof operator

Working on a function to generate update data for database updates, I encountered an issue.

The function takes in the following arguments:

  • the record to be updated
  • the property key
  • a new array item

Despite using keyof R to restrict the key's type, I faced an error when trying to assign a new object with that key to a Partial<R> constant. The error message read:

Type '{ [x: string]: any[]; }' is not assignable to type 'Partial<R>'.
How can I modify the code to resolve this issue? Replacing the generic type R with a non-generic type fixes the problem, but that solution doesn't meet my requirements.

Snippet on TypeScript Playground

interface BaseRecord {
    readonly a: ReadonlyArray<string>
}

function getUpdateData<R extends BaseRecord>(record: R, key: keyof R, newItem: string) {
    const updateData: Partial<R> = { [key]: [...record[key], newItem] }
    return updateData
}

interface DerivedRecord extends BaseRecord {
    readonly b: ReadonlyArray<string>
    readonly c: ReadonlyArray<string>
}
const record: DerivedRecord = { a: [], b: [], c: ["first item in c"] }
console.log(getUpdateData<DerivedRecord>(record, "c", "second item in c"))

Answer №1

There are two ways to manipulate the type system in your favor: using cunning techniques like index access and making the compiler assume R[key] is read-write,

function getUpdateData<R extends BaseRecord>(record: R, key: keyof R, newItem: string) {
    var updateData: Partial<R> = {};
    updateData[key] = [...record[key], newItem]; 
    return updateData
}

or taking the brute force approach by passing through the any type:

function getUpdateData<R extends BaseRecord>(record: R, key: keyof R, newItem: string) {
    const updateData: Partial<R> = <any> { [key]: [...record[key], newItem] }
    return updateData
}

The methods above address your query, but caution is advised: this function is not foolproof. It assumes that any record provided will contain a string[] value for the key property, which may not always be the case with the type R. For instance:

interface EvilRecord extends BaseRecord {
    e: number;
}
var evil: EvilRecord = { a: ['hey', 'you'], e: 42 };
getUpdateData(evil, 'e', 'kaboom');  // compiles without errors but leads to runtime issues 

Furthermore, the return type Partial<R> is broad and you need to specifically check for the existence of the key property to satisfy the type system:

var updatedData = getUpdateData<DerivedRecord>(record, "c", "first item in c") // Partial<DerivedRecord>
updatedData.c[0] // warning, object might be undefined

I recommend typing getUpdateData() as follows:

type KeyedRecord<K extends string> = {
    readonly [P in K]: ReadonlyArray<string>
};

function getUpdateData<K extends string, R extends KeyedRecord<K>=KeyedRecord<K>>(record: R, key: K, newItem: string) {
    return <KeyedRecord<K>> <any> {[key as string]:  [...record[key], newItem]};
}

(note that this remains challenging due to an issue in TypeScript) By implementing this change, the function will only accept inputs where the key property is of type ReadonlyArray<string>, ensuring the presence of the key property in the output:

var evil: EvilRecord = { a: ['hey', 'you'], e: 42 };
getUpdateData(evil, 'e', 'kaboom'); // error, the number is not a string array

var updatedData = getUpdateData(record, "c", "first item in c") // KeyedRecord<"c">
updatedData.c[0] // no error

I hope this clarifies things.


Technical Update

I revised the suggested declaration of getUpdateData() to incorporate two generic parameters. This was necessary because TypeScript was previously inferring an overly generalized type for the key parameter, requiring manual specification of the key type during invocation:

declare function oldGetUpdateData<K extends string>(record: KeyedRecord<K>, key: K, newItem: string): KeyedRecord<K>;
oldGetUpdateData(record, "c", "first item in c"); // The inferred type is 'a'|'b'|'c', despite specifying 'c'
oldGetUpdateData<'c'>(record, "c", "first item in c"); // Now works correctly 

By introducing a second generic parameter, the correct inference of the key type precedes the determination of the record type, addressing the previous inference discrepancy:

getUpdateData(record, "c", "hello"); // Inference correctly identifies 'c' now

You can overlook these details, but it sheds light on how TypeScript's heuristic type inference operates.

Answer №2

It is not possible to declare a constant of generic type.

Therefore, the modifications to your code should be as follows:

function getUpdateData<R extends BaseRecord>(record: R, key: keyof R, newItem: string) {
    const updateData:Partial<DerivedRecord> = {
        [key]: [...record[key], newItem]
    };

or

function getUpdateData<R extends BaseRecord>(record: R, key: keyof R, newItem: string) {
    const updateData = {
        [key]: [...record[key], newItem]
    };

or

function getUpdateData<R extends BaseRecord>(record: R, key: keyof R, newItem: string) {
   type PartRecord = Partial<DerivedRecord>;
   const updateData: PartRecord = {
    [key]: [...record[key], newItem]
};

return updateData;
}

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

Require using .toString() method on an object during automatic conversion to a string

I'm interested in automating the process of utilizing an object's toString() method when it is implicitly converted to a string. Let's consider this example class: class Dog { name: string; constructor(name: string) { this.name = na ...

Efficient method of triggering an action on a subcomponent in React Redux without the need to pass props down the component tree

Currently in the process of learning how to utilize react, redux, and react-redux with a straightforward requirement. I aim to display something similar to the layout below... -------------------------------- | title 1 |----------| | | descriptio ...

Error in React: Trying to access property 'functionName' of an undefined object on click event

I am facing an issue while trying to click a button in my React component that is supposed to trigger a function with a parameter named "item" which is defined within the function. The pseudo-HTML snippet for this scenario looks like: <div>{item.cre ...

Iterate through values in a Typescript loop

Incorporating nextjs, React, and Typescript into my project. I am aiming to extract values from an array of objects. Here is a snippet of my data in data.json: [ { "id": "value", “name”: “value”, “type”: “value” } ...

The freshly created object shall be devoid of any contents

Help needed with troubleshooting this code. import "reflect-metadata"; export class Castable { [key: string]: any; constructor(source: any) { console.log("source: "); console.log(source); Object.getOwnPropertyNames(sour ...

Preventing me from instantiating objects

I've been struggling with an issue for a while now consider the following: export abstract class abstractClass { abstract thing(): string } export class c1 extends abstractClass { thing(): string { return "hello" } } export cla ...

How can I clear the defaultValue attribute for an input field when submitting a form?

I created a component with the following code: import * as React from 'react'; const taskAdd = (props: { handleAdd: any }) => { return ( <form className="form-inline" onSubmit={props.handleAdd}> <tab ...

Choose a file in React by specifying its path instead of manually picking a file

Is there a way for me to automatically select a file from a specified path into my state variable without having to open a select file dialog? I'm looking for a solution where I can bypass the manual selection process. Any suggestions on how this can ...

Querying the api for data using Angular when paginating the table

Currently, I have a table that retrieves data from an API URL, and the data is paginated by default on the server. My goal is to fetch new data when clicking on pages 2, 3, etc., returning the corresponding page's data from the server. I am using an ...

Using the Amazon Resource Name (ARN) of a Cloud Development Kit (CDK) resource in a different

Having trouble obtaining the ARN of my AWS CDK stack's Step Functions state machine for my lambda function. The ARN is constantly changing and I'm unsure how to access it. I attempted to create a .env file alongside the lambda function's in ...

What could be causing my promise chain to fail to resolve?

Recently, I started using promises and came across an issue with a method in my VUE component. Upon debugging, it was evident that the API was returning data and reaching the commented line. However, upon the function's return, it failed to reach the ...

Is there a way to omit type arguments in TypeScript when they are not needed?

Here is a function I am currently working with: function progress<T>(data: JsonApiQueryData<T>): number { const { links, meta } = data.getMeta(); if (!links.next) { return 1; } const url = new URL(links.next); return parseInt(url ...

Navigating through the Angular Upgrade Roadmap: Transitioning from 5.0 to 6

As per the instructions found in this helpful guide, I executed rxjs-5-to-6-migrate -p src/tsconfig.app.json. However, an error is appearing. All previous steps were completed successfully without any issues. Any suggestions on how to resolve this? Please ...

Exploring Java's Generics and Wildcards within Collections

While working on a test class with AssertJ, my code looks like this: public void someTest() { assertThat(getNames()).has(sameNamesAs(getExpectedNames())); assertThat(getNames()).doesNotHave(sameNamesAs(getOtherNames())); } private List<String& ...

Error message stating: "The 'MktoForms2' property is not recognized within the scope of 'Window & typeof globalThis'."

Encountering the following error message: (Property 'MktoForms2' does not exist on type 'Window & typeof globalThis') while working with react and typescript useEffect(() => { window.MktoForms2.loadForm("//app-sj11.marke ...

Ways to retrieve a particular element within an array

I am looking to access each individual element of the "to" value within the children property of my array. const items = [{ name: 'Administrator', children: [ { name: 'Catalog', to: 'catalog' }, and he ...

Exploring the different categories within object destructuring

Here's an interesting scenario: const { foo: IFoo[] } = bar; This situation, along with the following: const { foo: Array<IFoo> } = bar; could potentially lead to errors. However, if you have something like this: const { foo: TFoo } = bar; ...

Tips for avoiding the 'Duplicate keys detected' error when using a v-for loop in Vue.js

In my most recent project, I utilized Nuxt.Js along with Vuetify.js as the UI framework. The language I used was TypeScript. As part of this project, I attempted to create the image below using arrays. https://i.sstatic.net/t1Xsc.jpg export const dumm ...

The callback function `(err: any, data: any) => void` does not share any properties with the type `RequestInit`

Inspired by the tutorial at , I am working on a time-based visualization. I am currently using version "d3": "^5.4.0". Here is the code snippet: d3.json('http://127.0.0.1:5000', function (err, data) { if (err) throw err; // Cre ...

In Ionic 2, any modifications made to the data model will only be reflected in the user interface after there is

Q) Why does my data seem to magically appear on the UI after interacting with it, even though I have made changes in the backend? For instance, when fetching and updating data bound to a list, such as: this._LocalStorageService.getClients().then( (data ...