9

Let's say the interface has some known properties with their types, and can have additional ones with unknown keys and some other types, something like:

interface Foo {
  length: number;
  [key: string]: string;
}

const foo : Foo = {
  length: 1,
  txt: "TXT",
};

TS error:

Property 'length' of type 'number' is not assignable to string index type 'string'.

How should such an interface be typed?

mbehzad
  • 3,758
  • 3
  • 22
  • 29
  • See also: https://stackoverflow.com/questions/47592546/how-to-combine-declared-interface-properties-with-custom-index-signature and https://basarat.gitbook.io/typescript/type-system/index-signatures – Christoph Lütjen Aug 15 '20 at 20:06

2 Answers2

9

[key: string]: string; this prevents the Foo interface to have none string properties (in your example, number).

What you can do is to separate the known and unknown properties in two interfaces and define your target type as a union type like below:

interface Foo {      
  length: number;
}

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

type FooBar = Foo | Bar;

const foo : FooBar = {
  length: 1, // its ok defined in foo
  txt: "TXT", // string is ok
  baz : 3 // error because its not string
};

Playground Link

Eldar
  • 9,781
  • 2
  • 10
  • 35
  • Why union and not intersection ? I don't even understand how it can work with union. Thank you. – Vincent J Oct 02 '21 at 07:36
  • 1
    @VincentJ Union of a and b means this type is either a or b. But intersection means type c is a combined version of a and b. So it has to satisfy every constraint in both types. So if we intersect Foo and Bar here we would have OP's version of Foo which tries to have both constraints that work against each other. (a length property type of number and string indexed string properties) – Eldar Oct 02 '21 at 13:16
  • if i write it with an intersection it gives the same result as your version (= only error on baz:3). – Vincent J Oct 02 '21 at 17:09
  • 1
    @VincentJ check [this](https://www.typescriptlang.org/play?ts=4.4.3#code/JYOwLgpgTgZghgYwgAgGIHt3IN7L-gKDwBsIQBzMACwC5kQBXAWwCNoBuAgXwINEliIUAIThQcRfAG0A1hACedAM5goocgF1lq9ck48CYeQAcUGdKPEBeNJmQAfZJc6GTZzJYBMyG+eQAyJzEXBHQQFWQYOzpzSx8JEjJKWmQARgAaZAB6LORgMCVkdBlkABMIGFAIUryQSMxJMAAPMDoAIgAVAA0Otsyc5BU1CjzC4skWOAAvZDoAZmzc6Ch0cTYEOAYlFHzCkHQwQZ0KbhCwiKj0bxiPMW8bbElSCmo6DMW8gqKS8sqQatq9XQjRa7W6vX6uSGumAYxkpyAA) out. It gives the same result because of the order of the checks being made. If you remove baz property it should be valid as you suggest but you will see the same error in the question. – Eldar Oct 03 '21 at 10:45
  • How can the values of ```foo``` be accessed? ```foo.txt``` or ```foo.baz``` don't seem to be recognized by TypeScript as valid values. – Ovidijus Parsiunas May 27 '22 at 10:11
  • This is not true, @OvidijusParsiunas. TS *does* recognize them, but `FooBar` is a *union*, and you can access only *shared* members of object types in unions. You need to *narrow* the type first with a type guard. – Oleg Valter is with Ukraine May 27 '22 at 10:18
  • `type BarValue = FooBar[string]` gives the error `Type 'FooBar' has no matching index signature for type 'string'` – joshkarges May 03 '23 at 23:30
  • 1
    @joshkarges as mentioned in the above comments won't work because `FooBar` is a union type and by the time of writing the only known key type is `length` and `string` doesn't extends it. Even if you use `length` property you will get a union type as a result. You need to convince TS that you are indexing the `Bar` type directly or by using some utility types like `Exclude` etc. – Eldar May 04 '23 at 13:21
  • 1
    additional advice is to read up on how [discriminated unions](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions) work in TypeScript, @joshkarges. Reiterating what's been said multiple times in the discussion: if you aren't doing any narrowing, there obviously will be an error. – Oleg Valter is with Ukraine May 04 '23 at 13:47
3

Check out this snippet it explain the point well

interface ArrStr {
  [key: string]: string | number; // Must accommodate all members

  [index: number]: string; // Can be a subset of string indexer

  // Just an example member
  length: number;
}

You can check this article for more details (i picked the snippet from there)

https://basarat.gitbook.io/typescript/type-system/index-signatures

For this element

Having both string and number indexers
This is not a common use case, but TypeScript compiler supports it nonetheless. However, it has the restriction that the string indexer is more strict than the number indexer. This is intentional e.g. to allow typing stuff like:

Mohamed Allal
  • 17,920
  • 5
  • 94
  • 97