Broaden the scope of a `Record<string, string[]>` by adding a new type of property

When working in Typescript, it appears that defining the type as shown below should create the desired outcome:

interface RecordX extends Record<string, string[]> {
  id: string
}

However, an error is thrown stating:

Property 'id' of type 'string' is not assignable to string index type 'string[]'. ts(2411)

Is there a way to include a property with a different type within a Record<> utility type?

Exploring Details and General Scenarios + Example

In general, how might one define an object with fixed properties of varying value types, alongside dynamically added properties with consistent value types?

For instance, consider the following object:

const a = {
   // some properties with predefined types
   id: '123',
   count: 123,
   set: new Set<number>(),

   // and some dynamic properties
   dynamicItemList: ['X', 'Y']
   anotherDynamicallyAddedList: ['Y', 'Z']
} as ExtensibleRecord

Can we define a type or interface ExtensibleRecord where:

  1. The types and keys of id, count, and set are fixed as string, number, and Set<number>
  2. The types of dynamicItemList, anotherDynamicallyAddedList, and any future dynamically added properties are all string[]

I have experimented with various approaches, such as:

type ExtensibleRecord = {
  id: string, count: number, set: Set<number>
} & Record<string, string[]>

type ExtensibleRecord = {
  id: string, count: number, set: Set<Number>
} & Omit<Record<string, string[]>, 'id'|'count'|'set'>

interface ExtensibleRecord = {
  id: string,
  count: number,
  set: Set<number>,
  [k: string]: string[]
}

Unfortunately, each attempt results in errors.

This seems like a common and straightforward issue, but I am unable to locate an example or reference for guidance.

playground

Answer №1

As per the information provided in the official documentation, achieving the desired outcome may not be possible:

The documentation highlights that while string index signatures can be used to represent the "dictionary" pattern effectively, they also require all properties to match their designated return type. This restriction is imposed because a string index asserts that obj.property should also be accessible as obj["property"]. In cases where the type of 'name' does not align with the string index’s type, an error will be flagged by the type system, as illustrated below:

interface NumberDictionary {
    [index: string]: number;
    length: number;    // valid, as length is a number
    name: string;      // error, since 'name' type isn't a subtype of the indexer
}

Nonetheless, an alternate perspective presented on this specific source suggests that excluding certain properties from the index signature is conceivable under particular scenarios. Such exclusion is particularly applicable when modeling a declared variable. Let me quote the relevant section from the same source.

Sometimes, there arises a need for amalgamating properties into the index signature which isn't usually recommended. Ideally, one should resort to the Nested index signature approach as indicated previously. Nevertheless, for situations involving pre-existing JavaScript constructs, such a combination can be achieved via an intersection type. The subsequent example illustrates the issue you might face without employing an intersection:

type FieldState = {
  value: string
}

type FormState = {
  isValid: boolean  // Error: Fails to adhere to the index signature
  [fieldName: string]: FieldState
}

Below demonstrates a workaround utilizing an intersection type:

type FieldState = {
  value: string
}

type FormState =
  { isValid: boolean }
  & { [fieldName: string]: FieldState }

It's important to note that while the above declaration caters to modeling existing JavaScript patterns, generating such an object directly within TypeScript remains unfeasible:

type FieldState = {
  value: string
}

type FormState =
  { isValid: boolean }
  & { [fieldName: string]: FieldState }


// Applying it to an external JavaScript object
declare const foo:FormState; 

const isValidBool = foo.isValid;
const somethingFieldState = foo['something'];

// Attempting to create a TypeScript object using these types will lead to an error
const bar: FormState = { // Error `isValid` not assignable to `FieldState
  isValid: false
}

Lastly, if deemed appropriate, a workaround involves establishing a nested interface structure (refer to the section "Design Pattern: Nested index signature" on this source) wherein the dynamic fields are encapsulated within a property of the interface. For instance:

interface RecordX {
  id: string,
  count: number,
  set: Set<number>,
  dynamicFields: {
    [k: string]: string[]
  }
}

Answer №2

If you're not utilizing it for a class, you can define it using the type:

type RecordWithID = Record<string, string[]> & {
  id: string
}

let x: RecordWithID = {} as any

x.id = 'abc'

x.abc = ['some-string']

Click here to view in TypeScript Playground

Answer №3

Pedro's response was spot on and marked the beginning of my journey - thank you!

I was on a quest to find the most straightforward solution for this issue, and stumbled upon a workaround: Object.assign(). It appears to be immune to the type errors that arise when manually constructing these objects or using the spread operator, possibly due to its compatibility with TypeScript as a native JavaScript feature.

The idea of encapsulating Object.assign() in a factory function alongside a parallel type seems quite elegant to me; it might prove beneficial to others as well.

// FACTORY WITH TYPE

// The factory utilizes Object.assign(), which avoids errors, and yields an intersection type
function RecordX(
  fixedProps: { id: string },
  dynamicProps?: Record<string, string[]>
) {
  return Object.assign({}, dynamicProps, fixedProps);
}

// The name of the type conveniently aligns with the factory and equates to:
// 
// type RecordX = Record<string, string[]> & {
//   id: string;
// }
type RecordX = ReturnType<typeof RecordX>;

// USAGE

// Regular usage
const model: RecordX = RecordX({ id: 'id-1' }, {
  foo: ['a'],
});

// Correct type: string
const id = model.id;

// Correct type: string[]
const otherProp = model.otherProp;

// Appropriately throws an error stating "string is not assignable to string[]"
const model2: RecordX = RecordX({ id: 'id-2' }, {
  bar: 'b'
});

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

Implement an interface with a specific number of properties in TypeScript

I am attempting to create a custom type that defines an object with a specific number of key-value pairs, where both the key and value are required to be numbers. Here is what I envision: type MatchResult = { [key: number]: number; [key: number]: numbe ...

What sets typescript apart when using a getter versus a regular function?

Imagine having a class with two methods declared for exclusive use within that class. // 1. private get something() { return 0; } // 2. private getSomething() { return 0; } While I am familiar with getters and setters, I'm intrigued to know if ther ...

Having difficulty implementing a versatile helper using Typescript in a React application

Setting up a generic for a Text Input helper has been quite challenging for me. I encountered an error when the Helper is used (specifically on the e passed to props.handleChange) <TextInput hiddenLabel={true} name={`${id}-number`} labelText=" ...

What is the best way to design a circular icon using OpenLayers?

I am currently incorporating openlayers into my ionic app and working on placing users on the map. I have encountered a problem as I am unsure how to apply custom CSS styling to the user element, which is not directly present in the HTML. In the screenshot ...

Can you explain the purpose of the curly braces found in a function's parameter definition?

I am currently working on an Angular 5 project and came across this intriguing Typescript code snippet. (method) CreateFlightComponent.handleSave({ currentValue, stepIndex }: { currentValue: Partial<Flight>; stepIndex: number; }): void Can ...

Angular 14: Enhance Your User Experience with Dynamic Angular Material Table Row Management

My inquiry: I have encountered an issue with the Angular material table. After installing and setting up my first table, I created a function to delete the last row. However, the table is not refreshing as expected. It only updates when I make a site chang ...

What is the best way to reproduce the appearance of a div from a web page when printing using typescript in Angular 4?

Whenever I try to print a div from the webpage, the elements of the div end up on the side of the print tab instead. How can I fix this issue? Below is my current print function: print(): void { let printContents, popupWin; printContents = document.getEl ...

Creating a different type by utilizing an existing type for re-use

Can you help me specify that type B in the code sample below should comprise of elements from interface A? The key "id" is mandatory, while both "key" and "value" are optional. interface A { id: string; key: string; value: string | number; } /** ...

Six Material-UI TextFields sharing a single label

Is there a way to create 6 MUI TextField components for entering 6 numbers separated by dots, all enclosed within one common label 'Code Number' inside a single FormControl? The issue here is that the label currently appears only in the first tex ...

Tips on sorting an array within a map function

During the iteration process, I am facing a challenge where I need to modify certain values based on specific conditions. Here is the current loop setup: response.result.forEach(item => { this.tableModel.push( new F ...

The property 'enabled' is not a standard feature of the 'Node' type

Within the code snippet below, we have a definition for a type called Node: export type Node<T> = T extends ITreeNode ? T : never; export interface ITreeNode extends TreeNodeBase<ITreeNode> { enabled: boolean; } export abstract class Tre ...

Merging two arrays concurrently in Angular 7

When attempting to merge two arrays side by side, I followed the procedure below but encountered the following error: Cannot set Property "account" of undefined. This is the code in question: acs = [ { "account": "Cash In Hand", ...

Is it possible to conceal my Sticky Div in MUI5 once I've scrolled to the bottom of the parent div?

Sample link to see the demonstration: https://stackblitz.com/edit/react-5xt9r5?file=demo.tsx I am looking for a way to conceal a fixed div once I reach the bottom of its parent container while scrolling down. Below is a snippet illustrating how I struct ...

The error message "TypeScript is showing 'yield not assignable' error when using Apollo GraphQL with node-fetch

Currently, I am in the process of implementing schema stitching within a NodeJS/Apollo/GraphQL project that I am actively developing. The implementation is done using TypeScript. The code snippet is as follows: import { makeRemoteExecutableSchema, ...

Can ngFor be utilized within select elements in Angular?

I'm facing an interesting challenge where I need to display multiple select tags with multiple options each, resulting in a data structure that consists of an array of arrays of objects. <div class="form-group row" *ngIf="myData"> <selec ...

What is the process for running a continuous stream listener in a node.js function?

I am currently working with a file called stream.ts: require('envkey') import Twitter from 'twitter-lite'; const mainFn = async () => { const client = new Twitter({ consumer_key: process.env['TWITTER_CONSUMER_KEY'], ...

Exploring Angular 10 Formly: How to Retrieve a Field's Value within a Personalized Formly Wrapper

Utilizing Angular 10, I have a formly-form with a select-field named session. This select field provides options from which to choose a dndSession. Each option holds key-value pairs within an object. I want to add a button next to the select-field that tr ...

The object in an Angular 11 REACTIVE FORM may be null

I am looking to incorporate a reactive form validation system in my application, and I want to display error messages based on the specific error. However, I am encountering an error that says: object is possibly 'null'. signup.component.html &l ...

If the value is null, pass it as is; if it is not null, convert it to a date using the

I am currently facing an issue regarding passing a date value into the rrule plugin of a fullCalendar. Here is the snippet of code in question: Endate = null; rrule: { freq: "Daily", interval: 1, dtstart: StartDate.toDate ...

When using a function as a prop in a React component with Typescript generics, the type of the argument becomes unknown

React version 15 or 18. Typescript version 4.9.5. When only providing the argument for getData without using it, a generic check error occurs; The first MyComponent is correct as the argument of getData is empty; The second MyComponent is incorrect as t ...