I'm finding that Typescript generics are not resolving concrete types to my satisfaction

In my unique class implementation, I store a list of items in an object map rather than a flat array. Each property in the object map represents a specific group of items, similar to grouping cars by manufacturer.

// Definition of object map interface
interface IObjectMap<TValue> {
    [key: string]: TValue;
}

// Definition of item map 
type ItemMap<TMapKeys extends keyof IObjectMap<unknown>, TValue> = Record<TMapKeys, TValue[]>;

// Definition of getter function type
type GetterFunc<TInput, TResult> = (item: TInput) => TResult;

// Definition of getter map for object properties
type GetterMap<TMapKeys extends keyof IObjectMap<unknown>, TInput> = Record<TMapKeys, GetterFunc<TInput, string>>;

class GroupedItems<
    TItem, // Type of items
    TGroupKeys extends keyof IObjectMap<unknown> // Object map keys
> {
    public groups: ItemMap<TGroupKeys, TItem> = {} as ItemMap<TGroupKeys, TItem>;
    public countryGetters: GetterMap<TGroupKeys, TItem> = {} as GetterMap<TGroupKeys, TItem>;

    public addItems(items: TItem[], getGroupKey: GetterFunc<TItem, TGroupKeys>): void {
        this.items.concat(items);
        this.items
            .forEach(item => {
                let name = getGroupKey(item);
                if (this.groups[name] === undefined) {
                    // Create placeholder for items
                    this.groups[name] = [];
                }
                // Assign the item to the group
                this.groups[name].push(item);
            });
    }

    public assignGetters(getters: GetterMap<TGroupKeys, TItem>) {
        this.countryGetters= getters;
    }
}

The two generic type parameters for the class are:

  • The type of items being grouped (e.g., Car)
  • The group keys (e.g., 'renault' | 'peugeot' | ...)

Both class members are defined as object maps:

  • groups stores items in group arrays
  • countryGetters is also an object map with properties matching item groups, but with functions that return the manufacturer's country of the car

Example of Usage

Although the code appears free of errors, TypeScript doesn't resolve types correctly during usage. It should raise warnings when attempting to use a group name not defined in the map or union type of group keys...

interface Car {
    model: string;
    year: number;
}

interface CarMakers<TValue> extends IObjectMap<TValue> {
    renault: TValue;
    peugeot: TValue;
}

let select = new GroupedItems<
    Car,
    keyof CarMakers<unknown>
>();

select.addItems([
        { model: 'R5', year: 1980, dummy: false }, // error; correct
        { model: '206', year: 2004 },
        { model: '3008', year: 2010 }
    ],
    car =>
        car.year < 2000
        ? 'audi' // Should be an error; "audi" not in "keyof MakerGroups<>"
        : 'peugeot'
);
select.assignGetters({
    renault: () => 'France',
    audi: () => 'Germany' // Should trigger an error; "audi" not in "keyof MakerGroups<>"
});

As depicted, the group names aren't resolved correctly by TypeScript, resulting in invalid manipulations on non-existent groups. The provided code looks fine from a compilation standpoint, but it needs additional checks and better intellisense support for filling up group names.

Here is a reference to the playground link for experimentation.

Answer №1

Upon inspecting the type of the select element you've created, it appears to be

GroupedItems<Car, string | number>
. The issue lies with TGroupKeys, as we intended for it to be 'renault' | 'peugeot' instead of string | number.

The IObjectMap interface includes an index signature, indicating that it should hold a value for ANY string. Since CarMakers extends IObjectMap, keyof CarMakers<unknown> encompasses every string (including numbers), and not just the specific keys of your object. This principle applies to any object extending IObjectMap.

Referencing the typescript documentation,

If a type has a string index signature, keyof T will include string | number (as in JavaScript, object properties can be accessed using strings or numbers).

To resolve this issue, the index signature needs to be removed entirely from all instances.

Rather than extending keyof IObjectMap<unknown>, both TMapKeys and

TGroupKeys</code should extend either <code>string
or PropertyKey -- which is a predefined type representing the union of valid property keys (string | number | symbol).

After making these adjustments, the desired errors are now displayed:

Type '"audi"' is not assignable to type '"renault" | "peugeot"'
Object literal may only specify known properties, and 'audi' does not exist in type 'Record<"renault" | "peugeot", GetterFunc<Car, string>>'

Playground Link

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

Guide on converting an array of objects into a single nested object

I am struggling with handling multiple levels of nesting in an array of objects, each specifying its parent's name. Is there a way to consolidate all this information into a single object? Here's the desired output format: { "REPORTING PER ...

Creating versatile list components that can accommodate various types of list items

As part of my project using Next.js, typescript, and type-graphql, I found myself creating Table components. These components are meant to display custom object types as rows within a table. While each piece of data has its own unique structure, they all ...

Choose an option from a selection and showcase it

I need to implement a modal that displays a list of different sounds for the user to choose from. Once they select a sound, it should be displayed on the main page. Here is the code snippet for the modal page: <ion-content text-center> <ion-ca ...

What is the best way to output the leaf nodes from an array of object lists in TypeScript?

Having trouble with TypeScript, specifically working with arrays and filtering out leaf nodes. I want to print only the leaf nodes in the array, resulting in ['002', '004', '007']. Can someone please assist me? Excited to lear ...

The specified type '(Person | undefined)[]' cannot be assigned to the type 'People'

Encountering a typescript error while trying to update the state from the reducer: The error states: Type '(Person | undefined)[]' is not assignable to type 'People' reducer.ts: export type Person = { id: string; name: string; ph ...

The callback function in Typescript is returning an incorrect type guard with an additional undefined type

I developed a function called filterObject that takes an object and a callback function as parameters. The callback function includes Type declarations and a type guard, but unfortunately, the type guard isn't functioning correctly. The code for the ...

Harness the power of a global service with a customized pipe in Angular 2

Exploring the capabilities of Angular 2, I have developed a global service that houses an interface. Various components utilize this interface from the global service. When the interface is modified by one component, the changes are reflected in all child ...

Preserve JSON information following a Typescript get request

Currently, I am attempting to establish a connection with a remote server's REST API in order to retrieve some valuable data. This information will then be utilized in an Angular2 LineChart. I have successfully obtained the JSON file and converted it ...

Can the state property be utilized in onChange for react with typescript?

Let me introduce myself - I am transitioning from JavaScript to TypeScript in my React project. Here is the code snippet that includes my implementation of onChange: interface AddTodoState { text: string; } class AddTodo extends Component<AddTodoProp ...

Can you provide instructions on executing package dependencies using yarn in the command line? For example, is there a command similar to npx tsc init for initializing a npm

When utilizing yarn, the node_modules folder is not present. Instead, dependencies are stored in a .yarn/cache folder. I attempted to use yarn dlx tsc init and npx tsc init, but they did not achieve the desired result. There are various development depend ...

Discovering the RootState type dynamically within redux toolkit using the makeStore function

I am currently working on obtaining the type of my redux store to define the RootState type. Previously, I was just creating and exporting a store instance following the instructions in the redux toolkit documentation without encountering any issues. Howev ...

Typescript is unable to mandate the passing of generics

I am in need of a utility that can handle generic object types, taking a key belonging to that type and the associated property like this: export type StateBuilder = <StateSchema, Keys extends keyof StateSchema>( key: Keys, data: StateSchema[Keys ...

The message "Expected a string literal for Angular 7 environment variables" is

I'm currently working on setting up different paths for staging and production environments in my Angular project, following the documentation provided here. I have a relative path that works perfectly fine when hardcoded like this: import json_data f ...

What's the deal with Iterators and Const iterators in C++?

If I have two classes, let's consider the first one: class IntMatrix::iterator { private: const IntMatrix *int_matrix; int index; iterator(const IntMatrix *int_matrix, int index); friend class IntMatrix; public: int &operat ...

Can TypeScript allow for type checking within type definitions?

I've developed a solution for returning reactive forms as forms with available controls listed in IntelliSense. It works well for FormControls, but I'm now looking to extend this functionality to include FormGroups that are part of the queried pa ...

Having trouble getting Jest transformers to play nice with a combination of Typescript, Webpack, and PEGJS

In my current NPM project: { "scripts": { "test": "jest --config=jest.config.js" }, "devDependencies": { "@types/pegjs": "0.10.3", "@types/jest": "29.1.1", ...

In Typescript, the function is expected to return a string rather than using the syntax of `() -> string`

Currently, I am attempting to implement the following code snippet for browser detection. However, I am encountering an error in relation to browserData, which states: Type '{ browserName: () => string; browserVersion: string | null; operatingSys ...

For an unknown cause, the Angular dialog box is not showing up

I've been struggling to implement a dialog window in my project, but for some reason, it's not working as expected. Rather than opening the dialog window, the content of the dialog component is being added at the bottom of the page like an HTML e ...

Angular Reactive Forms - Adding Values Dynamically

I have encountered an issue while working with a reactive form. I am able to append text or files from the form in order to make an http post request successfully. However, I am unsure about how to properly append values like dates, booleans, or arrays. a ...

The type 'any' cannot be assigned to the type 'never' as a parameter

const [files, setFiles] = useState([]) const handleChange = (event: any) => { setFiles.push(event.target.files[0].name) return (<div> {files.map((file: any) => ( <p>Hello!</p> ))} </ ...