Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Extend a `Record<string, string[]>` with a different type of property

Tags:

typescript

In Typescript the following seems like it should accomplish the creation of the desired type:

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

but this complains about:

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

How can one add a property of a different type to a Record<> utility type?

Details and General Case + Sample

Generally, how can one describe an object with heterogenous-value-type fixed properties but homogenous-value-type properties that are dynamically added.

S, for example given this object:

const a = {
   // some properties with known "hard-coded" types
   id: '123',
   count: 123,
   set: new Set<number>(),

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

So how can one define a type or interface ExtensibleRecord where:

  1. the types and property keys of id, count, and set are fixed as string, number and Set<number>
  2. the types of dynamicItemList and anotherDynamicallyAddedList and any other properties added to the object are string[]

I've tried many variants that I'd think might work, including:

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[]
}

but each seems to result in errors.

This feels like something common and obvious, but I can't find an example or reference.

playground

like image 870
Brian M. Hunt Avatar asked Jul 25 '19 18:07

Brian M. Hunt


People also ask

What is the use of record<string> in typescript?

Record<string, string []> means that the id property, if it exists, must be a string []. You can't extend it with something that doesn't match that; you're trying to make an exception, not an extension, and TypeScript does not (yet) support that as a concrete type.

Is there a type constraint for a record type?

There's no generic constraint that requires a type to be a record. Records satisfy either the classor structconstraint. To make a constraint on a specific hierarchy of record types, put the constraint on the base record as you would a base class.

How to declare a record type without any positional properties?

A record type doesn't have to declare any positional properties. You can declare a record without any positional properties, and you can declare other fields and properties, as in the following example: public record Person(string FirstName, string LastName) { public string[] PhoneNumbers { get; init; } = Array.Empty<string>(); };

How do I create a record type with immutable properties?

Beginning with C# 9, you use the recordkeyword to define a reference typethat provides built-in functionality for encapsulating data. You can create record types with immutable properties by using positional parameters or standard property syntax: public record Person(string FirstName, string LastName);


2 Answers

If you are not using it for class, you can describe it using type:

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

let x: RecordWithID = {} as any

x.id = 'abc'

x.abc = ['some-string']

http://www.typescriptlang.org/play/#code/C4TwDgpgBAShDGB7ATgEwOoEtgAsCSAIlALywIqoA8AzsMpgHYDmANFLfcwNoC6AfFABkUAN4BYAFBQomVAC52dRk0kBfSZIA2EYFAAeCuEjRZchEqNVQAhtRsMQGiXoB0siwHJrAI3genrj7wFlxe1KgAZh48TkA

like image 194
unional Avatar answered Oct 11 '22 05:10

unional


From the official docs, it's not possible to achieve what you want:

While string index signatures are a powerful way to describe the “dictionary” pattern, they also enforce that all properties match their return type. This is because a string index declares that obj.property is also available as obj["property"]. In the following example, name’s type does not match the string index’s type, and the type checker gives an error:

interface NumberDictionary {
    [index: string]: number;
    length: number;    // ok, length is a number
    name: string;      // error, the type of 'name' is not a subtype of the indexer
}

However, according to this site, excluding certain properties from the index signature is possible in one situation: when you want to model a declared variable. I'm copying the entire section from the site.

Sometimes you need to combine properties into the index signature. This is not advised, and you should use the Nested index signature pattern mentioned above. However, if you are modeling existing JavaScript you can get around it with an intersection type. The following shows an example of the error you will encounter without using an intersection:

type FieldState = {
  value: string
}

type FormState = {
  isValid: boolean  // Error: Does not conform to the index signature
  [fieldName: string]: FieldState
}

Here is the workaround using an intersection type:

type FieldState = {
  value: string
}

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

Note that even though you can declare it to model existing JavaScript, you cannot create such an object using TypeScript:

type FieldState = {
  value: string
}

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


// Use it for some JavaScript object you are gettting from somewhere 
declare const foo:FormState; 

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

// Using it to create a TypeScript object will not work
const bar: FormState = { // Error `isValid` not assignable to `FieldState
  isValid: false
}

Finally, as a workaround, if it fits, you might create a nested interface (check the section "Design Pattern: Nested index signature" of site, in which the dynamic fields are in a property of the interface. For instance

interface RecordX {
  id: string,
  count: number,
  set: Set<number>,
  dynamicFields: {
    [k: string]: string[]
  }
}
like image 14
Pedro Arantes Avatar answered Oct 11 '22 04:10

Pedro Arantes