58

I have the following interfaces:

export interface Meta {
  counter: number;
  limit: number;
  offset: number;
  total: number;
}

export interface Api<T> {
  [key: string]: T[];
  meta: Meta; // error
}

Currently, I'm receiving the following error:

Property 'meta' of type 'Meta' is not assignable to string index type 'T[]'.

After searching a bit, I found this statement in TS docs:

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"].

Does it means that when I have a string index signature, I can't have any other variable without match this type?

Actually I can get rid of this error declaring the interface like this:

export interface Api<T> {
  [key: string]: any; // used any here
  meta: Meta;
}

Doing this, I lose the completely ability of type inference. Is there any way to do this without this ugly way?

dev_054
  • 3,448
  • 8
  • 29
  • 56

2 Answers2

56

You can use an intersection of two interfaces:

interface Api<T> {
    [key: string]: T[];  
}

type ApiType<T> = Api<T> & {
    meta: Meta;
}

declare let x: ApiType<string>;

let a = x.meta // type of `a` is `Meta`
let b = x["meta"]; // type of `b` is `Meta`

let p = x["someotherindex"] // type of `p` is `string[]`
let q = x.someotherindex // type of `q` is `string[]`
Saravana
  • 37,852
  • 18
  • 100
  • 108
  • Thanks. It was really helpful :) Just a simple note... here `declare let x: Api;` shouldn't be `declare let x: ApiType;`? – dev_054 Jul 23 '17 at 03:25
  • Yes it should be. – Saravana Jul 23 '17 at 03:37
  • 1
    Having this same issue when working with DefinitelyTyped defintions, when trying to extend an interface. Is there a way to extend an interface that has this string index? e.g. ` // main declaration interface Api { [key: string]: T[]; } // in extension file interface Api { // extend interface test: T; } declare let x: ApiType; let a = x.test; ` – Quango Aug 30 '17 at 13:23
  • taking from the method in this answer, its simply a matter of separating the string indexes and the properties/methods into separate classes, then mashing them together with &. example: type ChildType = ParentTypeStringIndexes & ParentTypeFunctions & ParentTypeProperties & {....}, where otherwise it would have looked like: interface ChildType extends ParentTypeStringIndexes, ParentTypeFunctions, ParentTypeProperties {....} – iedoc Feb 02 '18 at 14:14
  • OTOH we cannot do this extending ApiType like here https://stackoverflow.com/questions/23914271/typescript-interface-definition-with-an-unknown-property-key – ciekawy Feb 27 '18 at 22:52
  • 29
    This seems like hack/workaround for a more fundamental issue: that TypeScript interfaces do not have an "`additionalProperties`" mechanism like JSON Schema does. Your solution works when *defining* a type, but fails when *implementing* it. In your example, try assigning a value to `x`, like `x = { meta: new Meta(), someotherindex: ['abc'] }`. You'll get an error. I would post a link to TypeScript Playground, but it’s too long for SO comments. – chharvey Dec 28 '18 at 22:42
  • @chharvey exactly what i have just faced, i want to initialize the variable right away but getting the same error, any other solution to have the assignment working too? – Kesem David Mar 01 '19 at 10:31
  • @chharvey: I cannot make it fail, can you share an example? (with a url shorter you can share a playground). tinyurl.com/wq5kvza – tokland Jan 21 '20 at 17:12
  • 2
    @tokland : You forgot to assign a type to `x` when you initialized it. You should get an error when assigning it type `ApiType`. – chharvey Jan 27 '20 at 02:47
  • I was able to use "intersection" approach without declaring additional types to intersect. type AssetsType = { [key: string]: AssetType, } & { badges: { [key in TRarity | 'all']: number } } – evasyuk Dec 02 '21 at 13:50
14

The presented best solution didn't work when I've tried to implement this interface. I ended up nesting part with dynamic key. Maybe someone will find it useful:

interface MultichannelConfiguration {
  channels: {
    [key: string]: Configuration;
  }
  defaultChannel: string;
}
Fifciuux
  • 766
  • 5
  • 8