Is it possible to limit the items in a TypeScript array to only accept shared IDs with items in another array?

I'm creating an object called ColumnAndColumnSettings with an index signature. The goal is to limit the values of columnSettings so that they only allow objects with IDs that are found in columns.

type Column = {
  colId: string,
  width?: number,
  sort?: number
  something?: string
}

type ColumnSetting = Pick<Column, 'colId' | 'width' | 'sort'>;

const columnExample = { colId: '1', something: 'test'};
const columnSettingExample1 = { colId: '1'};
const columnSettingExample2 = { colId: '2'};

export const ColumnAndColumnSettings: {
  [key: string]: { columns: Column[]; columnSettings: ColumnSetting[]};
} = {
  people: { columns: [columnExample] , columnSettings: [columnSettingExample1] }, // ok
  boats: { columns:  [columnExample], columnSettings: [columnSettingExample1, columnSettingExample2] }, // ok but want this to error, 
  //columnSettingExample2 does not share id with any item in columns
}

Playground Link

Answer №1

Starting out: the compiler will only recognize that the colId property values are not just of type string if you provide a clue to track their literal types. The simplest way to do this is by using a const assertion when creating the objects:

const columnExample = { colId: '1', something: 'test' } as const;
/* const columnExample: {  readonly colId: "1";  readonly something: "test"; } */
const columnSettingExample1 = { colId: '1' } as const;
/* const columnSettingExample1: {  readonly colId: "1"; } */
const columnSettingExample2 = { colId: '2' } as const;
/* const columnSettingExample2: {  readonly colId: "2"; } */

Now, the compiler identifies that columnExample has a colId of "1", not just any string.

Next up:


Unfortunately, TypeScript doesn't have a specific type that behaves like how you want ColumnAndColumnSettings to act.

Instead, you could define a generic type such as ColumnAndColumnSettings<T>, where T maps the keys of your object to the allowable column ids for that property:

type ColumnAndColumnSettings<T extends Record<keyof T, string>> = {
  [K in keyof T]: {
    columns: (Column & { colId: T[K] })[],
    columnSettings: (ColumnSetting & { colId: T[K] })[]
  }
}

This leads to something like this:

type Example = ColumnAndColumnSettings<{ cars: "3" | "4", trucks: "5" }>;
/* type Example = {
    cars: {
        columns: (Column & {
            colId: "3" | "4";
        })[];
        columnSettings: (ColumnSetting & {
            colId: "3" | "4";
        })[];
    };
    trucks: {
        columns: (Column & {
            colId: "5";
        })[];
        columnSettings: (ColumnSetting & {
            colId: "5";
        })[];
    };
} */

However, manually providing the T argument isn't ideal. It would be better if the compiler could infer it for you. While direct inference via type annotation isn't possible, there's a workaround involving a generic helper identity function specifically designed for inferring the type parameter. Instead of

const value: GenericType<infer> = { something: 123 }
, you'd create a helper function like
const asGenericType = <T,>(g: GenericType<T>) => g
and use it as
const value = asGenericType({something: 123})
.

In this scenario, we can write:

const asColumnAndColumnSettings = <T extends Record<keyof T, string>>(
  c: ColumnAndColumnSettings<T>) => c;

And now it can be utilized as follows:

const c = asColumnAndColumnSettings({    
  cars: { columns: [ce3, ce4], columnSettings: [ce3] },
  trucks: { columns: [ce5], columnSettings: [ce5] },
});
/* const c: ColumnAndColumnSettings<{
  cars: "3" | "4";
  trucks: "5";
}> */

So, the desired T is inferred successfully. Excellent!


Unfortunately, it's still not entirely correct:

const ColumnAndColumnSettings = asColumnAndColumnSettings({
  people: { columns: [columnExample], columnSettings: [columnSettingExample1] }, // works fine
  boats: { columns: [columnExample], 
    columnSettings: [columnSettingExample1, columnSettingExample2] }, // no error
});
/* const ColumnAndColumnSettings: ColumnAndColumnSettings<{
  people: "1";
  boats: "1" | "2";
 }> */

The absence of an error in the above example defeats the purpose. The compiler managed to infer T from both the columns property and the columnSettings property. What you truly desire is for T to be inferred solely from columns, with the columnSettings property being checked against it.

In other words, in the definition of ColumnAndColumnSettings<T> within the columnSettings property, T should be used in a non-inferential manner. Unfortunately, TypeScript doesn't support this directly, but there is an open issue on GitHub regarding this at microsoft/TypeScript#14829. Fortunately, there are workarounds mentioned there. Here, the approach taken is to intersect the type parameter usage with the empty object type {} to lower its inference priority, which is recommended:

type ColumnAndColumnSettings<T extends Record<keyof T, string>> = {
  [K in keyof T]: {
    columns: (Column & { colId: T[K] })[],
    columnSettings: (ColumnSetting & { colId: T[K] & {} })[]
// ----------------------------------------------> ^^^^
  }
}

It operates similarly, and

ColumnAndColumnSettings<{ cars: "3" | "4", trucks: "5" }>
remains unchanged. However, now:

const ColumnAndColumnSettings = asColumnAndColumnSettings({
  people: { columns: [columnExample], columnSettings: [columnSettingExample1] }, // works fine
  boats: { 
    columns: [columnExample], 
    columnSettings: [columnSettingExample1, columnSettingExample2] }, // error! 
// ---------------------------------------> ~~~~~~~~~~~~~~~~~~~~~
// Types of property 'colId' are incompatible.
});

// const ColumnAndColumnSettings: ColumnAndColumnSettings<{ people: "1"; boats: "1"; }>

You now receive the expected error! The entry in T for boats is simply "1" instead of "1" | "2", leading to a flag for the columnSettingExample2 element with an incompatible colId property.


Playground link to code

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

Version 5 of angularfie2 is encountering an issue where the type 'Observable<{}[]>' cannot be assigned to the type 'Observable<any[]>'

Encountering an error while using angularfire2 version 5: The error reads: "Type 'Observable<{}[]>' is not assignable to type Observable < any [] >." Code snippet: exercisesList$: Observable <any[]>; ionViewDidLoad() { t ...

Leveraging Services in Classes Outside of Angular's Scope

I have a scenario where I am working with Angular v7.3.5 and I need to utilize a Service in a non-Angular class. Below is an example of what I'm trying to achieve: foo.model.ts import { Foo2Service } from './foo2.service'; // Definition fo ...

Angular 6: Dealing with Type Errors in the HttpClient Request

I am encountering issues with my services. I am attempting to retrieve a list of media files (generated by NodeJS, which generates a JSON file containing all media items). Although I can successfully fetch the data, I am facing an error stating "Property & ...

Is there a way to attach a ref to a Box component in material-ui using Typescript and React?

What is the best way to attach a ref to a material-ui Box component in React, while using TypeScript? It's crucial for enabling animation libraries such as GreenSock / GSAP to animate elements. According to material-ui documentation, using the itemRef ...

brings fulfillment except for categories

The new satisfies operator in TypeScript 4.9 is incredibly useful for creating narrowly typed values that still align with broader definitions. type WideType = Record<string, number>; const narrowValues = { hello: number, world: number, } sa ...

Issue arises when compiling React Redux due to a union type that includes undefined

I am currently in the process of learning how to integrate Redux with React using Typescript. I encountered a compilation error that relates to the type of my store's state, specifically as {posts: PostType[]}. The error message states: Type '{ p ...

Construct a string by combining the elements of a multi-dimensional array of children, organized into grouped

My task involves manipulating a complex, deeply nested array of nodes to create a specific query string structure. The desired format for the query string is as follows: (FULL_NAME="x" AND NOT(AGE="30" OR AGE="40" AND (ADDRESS ...

TypeScript is failing to identify a correctly typed property

Currently, I am facing issues while converting a Material UI Dashboard from React to Typescript. The problem arises during TypeScript compilation where the property in question meets the criteria mentioned in the error message. To put it briefly, the compi ...

Issue with Typescript flow analysis when using a dictionary of functions with type dependency on the key

I am utilizing TypeScript version 4.6 Within my project, there exists a type named FooterFormElement, which contains a discriminant property labeled as type type FooterFormElement = {type:"number",...}|{type:"button",...}; To create instances of these el ...

Using React with Typescript: Anticipating child component with particular props

I'm currently developing a component that necessitates the use of two specific child components. These two components are exported using dot notations from the main component and have defaultProps for identification within the main component: export ...

Angular does not propagate validation to custom form control ng-select

In my Angular 9 application, I am utilizing Reactive Forms with a Custom Form Control. I have enclosed my ng-select control within the Custom Form Control. However, I am facing an issue with validation. Even though I have set the formControl to be requir ...

Tips for utilizing <Omit> and generic types effectively in TypeScript?

I'm currently working on refining a service layer in an API with TypeScript by utilizing a base Data Transfer Object. To prevent the need for repetitive typing, I have decided to make use of the <Omit> utility. However, this has led to some per ...

Angular 7 error: No provider found for PagerService causing NullInjectorError

I have been struggling to get pagination working properly in my project. Below is the code I have written: pager.service.ts: import * as _ from 'underscore'; @Injectable({ providedIn: 'root', }) export class PagerService { ...

Sort Messages By Date Created

While using Vue, I encountered a general JavaScript question. I am fetching cat messages from an API for a chat interface. The initial call returns an array of objects where each object represents a chat message: data: [ {id: 1, created_at: "2022-05 ...

How to upgrade Angular Header/Http/RequestOptions from deprecated in Angular 6.0 to Angular 8.0?

When working on http requests in Angular 6.0, I typically follow this block of code. I attempted to incorporate the newer features introduced in Angular 8.0 such as HttpClient, HttpResponse, and HttpHeaders. However, I found that the syntax did not align ...

Changes in tabs are discarded when switching between them within Material UI Tabs

I have been experiencing an issue with the Material UI tab component where changes made in tabs are discarded when switching between them. It seems that after switching, the tabs are rendered again from scratch. For example, let's say I have a textFie ...

Having trouble understanding how to receive a response from an AJAX request

Here is the code that I am having an issue with: render() { var urlstr : string = 'http://localhost:8081/dashboard2/sustain-master/resources/data/search_energy_performance_by_region.php'; urlstr = urlstr + "?division=sdsdfdsf"; urlst ...

The Nextjs next-auth implementation with URL '/api/auth/callback/cognito' encountered a 502 Bad Gateway error while in production

I'm facing a challenge while trying to deploy a nextjs app that utilizes 'next-auth' with AWS Cognito. Interestingly, the app runs smoothly when tested locally using either next dev or next start. However, upon deploying it on the producti ...

Explore the information stored in two different arrays

Currently, I'm trying to access and load data from 2 arrays. I've managed to successfully load the items array within the first array. However, when attempting to access the data inside the second array, it fails to load. The JSON response conta ...

Alert: VirtualizedList warns of slow updates for a large list despite optimized components

Struggling with avoiding the warning message "VirtualizedList: You have a large list that is slow to update" while utilizing the <FlatList> component in React-Native. Despite thorough research and attempts at finding a solution, including referencin ...