8

If I define my types as follows, it follows my desired behavior.

interface Foo {}

interface Bar {
    a: string;
    b: boolean;
    c: Foo;
}

type x = keyof Bar; // "a" | "b" | "c"

However, if I try to add an index signature, it loses all my predefined members.

interface Bar {
    [index: string]: any;
}

type x = keyof Bar; // string | number

Is there a way to do this properly in TypeScript?

Something similar to:

type x = Exclude<Bar, { [index: string]: any }>; // never

EDIT I tried something similar to Jake's solution and got this:

interface Indexable<T> {
    [index: string]: any;
}
type BaseType<T> = T extends Indexable<infer U> ? U : never;

interface BaseFoo {
    Name: string;
}
interface Foo1 extends Indexable<BaseFoo> {}
type Foo2 = Indexable<BaseFoo>;

type base1 = BaseType<Foo1>; // {}
type base2 = BaseType<Foo2>; // BaseFoo

Foo1 does not work, for some reason the type info for that becomes {}. Foo2 does work, but intellisense does not say Foo2 for variables of type Foo2. They instead have Indexable<BaseFoo>.

I would really like to try to hide this type massaging from my users. And unfortunately, it's not feasible to ask them to cast back and forth from Indexable<T> to T.

Talbot White
  • 117
  • 1
  • 7
  • Don't think there is a way to do this. Once you add an index signature to the mix `keyof`will return `string ` so you can't get to the named keys and excluding the index signature is also not possible as any conditional type constraint that has a signature will also match the properties – Titian Cernicova-Dragomir Jul 12 '18 at 05:30
  • 7
    Possible duplicate of [TypeScript: remove index signature using mapped types](https://stackoverflow.com/questions/51465182/typescript-remove-index-signature-using-mapped-types) This question is technically older (by 10 days), but the other one has an answer. – Retsam Feb 04 '19 at 23:11
  • Thanks for linking other thread, it has a real answer! – Devin Rhode Apr 08 '22 at 13:04

2 Answers2

5

Answer

UPDATE: As of TS 4.1 there is a real solution: How to remove index signature using mapped types

Alternative

Get the keys before adding the index signature:

interface Foo {}

interface BarCore {
    a: string;
    b: boolean;
    c: Foo;
}


type Bar = BarCore & {
    [index: string]: any;
}

type X = keyof BarCore; // a|b|c

More

PS: try not to mix index signatures with valid prop at root level. Instead use the nested object pattern

Devin Rhode
  • 23,026
  • 8
  • 58
  • 72
basarat
  • 261,912
  • 58
  • 460
  • 511
0

No, because this is the correct behaviour. string | "x" will simplify to string because "x" extends string is true. You get string | number when you only defined a string index signature because JavaScript casts numeric indices to string indices on objects.

If you want the behavior you're looking for you're going to need to change your interface definitions.

interface Foo {}

interface Bar {
    a: string;
    b: boolean;
    c: Foo;
}

interface IndexedBar extends Bar {
    [ key: string ]: any;
}

type x = keyof Bar; // "a" | "b" | "c"

Also note that you won't get correct type-checking on IndexedBar in some cases.

function setValue(obj: IndexedBar, key: string, value: any): void {
    obj[key] = value;
}

setValue(bar, "a", 4); // No error, even though a is explicitly a string.
Ruby Tunaley
  • 351
  • 3
  • 4