Home > Blockchain >  How to create type with one or more keys with string value whilst allowing other fields of any type?
How to create type with one or more keys with string value whilst allowing other fields of any type?

Time:06-28

I am making a combobox component which takes two parameters:

  1. options: a series of objects to select from
  2. displaytextKeyname: a string which represents the name of a key to look at within each option which contains a string to display as the name

Here is in a simplified form:

type ComboboxProps<T> = {
    options: T[];
    displaytextKeyname: string;
}

const Combobox = ({
    options,
    displaytextKeyname
}:ComboboxProps<unknown>) => {
    options.forEach(o => console.log(o[displayTextKeyname])
}

I am looking to make an object that would be suitable as type T above. The idea is this is designed in such a generic way that said field could be called "description", "longname", "name" or whatever. All that is required is that it is a string.

Currently I have attempted it in the following ways:

Approach 1 - require all string values

type ComboboxOption = {
    [key: string]: string;
};

const foo: ComboboxOption = {
    a: "123",
    b: 456, // Type 'number' is not assignable to type string
};

Approach 2 - allow all values

type ComboboxOption = {
    [key: string]: unknown;
};

const foo: ComboboxOption = {
    a: "123",
    b: 123,
};

// This allows objects which do not have at least one key with a string value
const bar: ComboboxOption = {
    b: 123,
};

Approach 3 - extend interface

interface IComboboxOption {
    [key: string]: string;
}

//Interface 'IComboboxOptionExt' incorrectly extends interface 'IComboboxOption'.
  //'string' index signatures are incompatible.
    //Type 'unknown' is not assignable to type 'string'
interface IComboboxOptionExt extends IComboboxOption {
    [key: string | number | symbol]: unknown;
}

Is this beyond typescript's type system or am I just thinking about this entirely wrong?

CodePudding user response:

Yes, such a constraint is indeed too much for TypeScript types. A stand-alone type will not be able to express this behaviour.

But it is possible to achieve things like this with a generic function. This would be my approach:

function Combobox<
  Key extends string
>(key: Key, options: (Record<string, any> & Record<Key, string>)[]) {}

This will validate the passed array based on the constraint you specified.

Combobox("description", [{
  "description": "abc",
  a: 123
}])

Combobox("longname", [{
  "longname": "abc",
  a: 123
},{
  "longname": "efg",
  b: 456
}, {
  c: 123 // Error: '{ c: number; }' is not assignable to type 'Record<string, any> & Record<"longname", string>'
}])

Playground

CodePudding user response:

While @TobiasS.'s answer is perfectly valid, it is possible to build a solution that is much closer to your initial description:

  • using object destructuring, for a more React component-like signature (can be easily adapted to @TobiasS.'s answer as well)
  • using a standalone type, without necessarily applying it to a function (but only when used with a function can the generic be automatically inferred)
// Use a generic for the key instead of the array type
type ComboboxProps<Key extends string> = {
  displaytextKeyname: Key,
  // Constrain the array type using the key, like in @TobiasS.'s answer
  options: Array<Record<string, any> & { [k in Key]: string }>
}

Example using the type without a function:

// Using the type standalone,
// but we have to explicitly specify the generic type
const ComboboxObject: ComboboxProps<"hello"> = {
  displaytextKeyname: "hello",
  options: [{
    hello: "world",
    foo: 42
  }, {
    // @ts-expect-error
    hello: false // Type 'boolean' is not assignable to type 'string'.
  },
  // @ts-expect-error
  {
    foo: "bar" // Type '{ foo: string; }' is not assignable to type 'Record<string, any> & { hello: string; }'. Property 'hello' is missing in type '{ foo: string; }' but required in type '{ hello: string; }'.
  }]
}

Example using the type in a function:

// Using the type for a function argument,
// the generic can be automatically inferred.
const Combobox = <Key extends string>({
  options,
  displaytextKeyname
}: ComboboxProps<Key>) => {
  options.forEach(o => console.log(o[displaytextKeyname]))
}

Combobox({
  displaytextKeyname: "description",
  options: [{
    "description": "abc",
    any: true
  }]
})

Combobox({
  displaytextKeyname: "longname",
  options: [{
    "longname": "abc",
    a: 123
  }, {
    // @ts-expect-error
    "longname": 4, // Type 'number' is not assignable to type 'string'.
    b: 456
  },
  // @ts-expect-error
  {
    c: 123 // Type '{ c: number; }' is not assignable to type 'Record<string, any> & { longname: string; }'. Property 'longname' is missing in type '{ c: number; }' but required in type '{ longname: string; }'.
  }]
})

Playground Link

  • Related