36

Given an interface (from an existing .d.ts file that can't be changed):

interface Foo {
  [key: string]: any;
  bar(): void;
}

Is there a way to use mapped types (or another method) to derive a new type without the index signature? i.e. it only has the method bar(): void;

Laurence Dougal Myers
  • 1,004
  • 1
  • 8
  • 17

5 Answers5

53

Edit: Since Typescript 4.1 there is a way of doing this directly with Key Remapping, avoiding the Pick combinator. Please see the answer by Oleg where it's introduced.

type RemoveIndex<T> = {
  [ K in keyof T as
    string extends K
      ? never
      : number extends K
        ? never
        : symbol extends K
          ? never
          : K
  ]: T[K];
};

It is based on the fact that 'a' extends string but string doesn't extends 'a'.


There is also a way to express that with TypeScript 2.8's Conditional Types.

interface Foo {
  [key: string]: any;
  [key: number]: any;
  [key: symbol]: any;
  bar(): void;
}

type KnownKeys<T> = {
  [K in keyof T]:
    string extends K
      ? never 
      : number extends K 
        ? never
        : symbol extends K 
          ? never
          : K
} extends { [_ in keyof T]: infer U } ? U : never;


type FooWithOnlyBar = Pick<Foo, KnownKeys<Foo>>;

You can make a generic out of that:

// Generic !!!
type RemoveIndex<T extends Record<any,any>> = Pick<T, KnownKeys<T>>;

type FooWithOnlyBar = RemoveIndex<Foo>;

For an explanation of why exactly KnownKeys<T> works, see the following answer:

https://stackoverflow.com/a/51955852/2115619

Mihail Malostanidis
  • 2,686
  • 3
  • 22
  • 38
  • 5
    Brilliant! Thank you! It just goes to show, you should never doubt the powers of TypeScript. :D Here's the GitHub comment that seems to be the origin of this technique: https://github.com/Microsoft/TypeScript/issues/25987#issuecomment-408339599 – Laurence Dougal Myers Aug 23 '18 at 04:55
  • What about a recursive version of this type? Do you think it would be possible? Say, start with `{[k: string]: number, n: number, o: {[k: string]: number, n: number}}` and end up with `{n: number, o: {n: number}}`. – DanielM Dec 08 '18 at 13:16
  • @DanielM probably something like this - bit.ly/RecurRec But it seems to be saying that `B['o']['o']['n']` is a `{}` and not a number, which is weird. Try it in your code. – Mihail Malostanidis Dec 10 '18 at 00:51
  • 1
    There is only one level of "o", so it makes some sense that your example doesn't work. I made also a mistake on my initial type: the first index signature needs to point to `any` as opposed to `number` in order to allow for `o` to be defined as an object. With these two considerations... It seems to be working! goo.gl/xkwG33 (I will further test in at a different time.) – DanielM Dec 10 '18 at 09:37
  • @DanielM in my example I made it 2-deep to make sure it's actually recursive. But yeah, the problem could indeed be with the index signature conflict. – Mihail Malostanidis Dec 10 '18 at 11:33
  • 1
    OK, after a second look, all types are actually "lost in translation". When hovering over `B`, TypeScript says about `n` that is of type `MappedType>`, which I guess is a way to express that no properties are picked (it is actually not a valid type in itself). The bottom line is that your recursive solution allows to assign any type to `n`, e.g. `'abc'` would be a valid value. – DanielM Dec 14 '18 at 23:39
  • It's weird, because in your example, `B['o']` claims to be `type X1 = { n: MappedType>; }` whereas it should be `{n: number}`. `number` doesn't `extends Record` so it should be a `T[K]` :/ – Mihail Malostanidis Dec 16 '18 at 22:57
  • For some reason, this doesn't seem to be working on TypeScript 3.3.3 anymore... Has anyone noticed the same? – DanielM Feb 10 '19 at 14:27
  • @DanielM the example featured works on `3.4.0-dev.20190209` for me. Please provide what doesn't work. – Mihail Malostanidis Feb 11 '19 at 17:09
  • 1
    @MihailMalostanidis It was the generic version that I thought didn't work anymore. But I was missing `extends Record`! (And the error message wasn't very helpful for me.) Thanks, and sorry ^^' – DanielM Feb 13 '19 at 21:20
  • Is there a way to make this work with primitive types like `string`. I tried to use `RemoveIndex` but the primitive type still has the string indexing constraints. – CMCDragonkai Oct 16 '21 at 07:21
  • @CMCDragonkai you mean you want to make a type like `string` but that doesn't support indexing by `number`? I don't think that's possible right now. It makes `String` impossible to index, but I don't think there is a way to make a modified version of a primitive type right now. – Mihail Malostanidis Oct 16 '21 at 10:26
  • Yea I needed to trick TS to think of the type as a primitive string, but without the index signature that `String` has – CMCDragonkai Oct 18 '21 at 10:48
27

With TypeScript v4.1 key remapping leads to a very concise solution.

At its core it uses a slightly modified logic from Mihail's answer: while a known key is a subtype of either string, number, or symbol, the latter are not subtypes of the corresponding literal. On the other hand, string is a union of all possible strings (the same holds true for number) and thus is reflexive (type res = string extends string ? true : false; //true holds).

This means you can resolve to never every time the type string, number, or symbol is assignable to the type of key, effectively filtering it out:

interface Foo {
  [key: string]: any;
  [key: number]: any;
  bar(): void;
}

type RemoveIndex<T> = {
  [ K in keyof T as
    string extends K
    ? never
    : number extends K
    ? never
    : symbol extends K
    ? never
    : K
  ] : T[K];
};

type FooWithOnlyBar = RemoveIndex<Foo>; //{ bar: () => void; }

Playground

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Do you mind if I inline this into my answer (since it is the one marked accepted)? – Mihail Malostanidis Mar 17 '21 at 11:01
  • 4
    @MihailMalostanidis - you mean the type? Yeah, sure - hope someday we will have a better way to address that – Oleg Valter is with Ukraine Mar 18 '21 at 02:38
  • Sadly, while the old system worked for input types that had alternatives (`A | B`), it doesn't work with typescript 4.3, and nor does this. – Cheetah Jul 19 '21 at 19:42
  • In your example, the alternatives of the union have the same members, and so they collapse down to being effectively the same type. I've adjusted your example to show the issue: https://tsplay.dev/NBPegW. RemoveIndex results in a type that only accepts the keys the union members have in common, instead of a union of removing the indexer from the union members. (Edit: fixed a mistake in my example) – Cheetah Jul 20 '21 at 16:28
  • @Cheetah ah, this. Well, last time I checked, this is the normal behavior of unions with object-like members. If you use a type guard (see [playground](https://tsplay.dev/mbdGPw) based on your example), you will see that the resulting type is not lossy, and all the member information is still there. But I now see what you mean, the old version up to 4.2.3 did indeed [allow this](https://tsplay.dev/mAVvBW), but this looks like an unintended side-effect rather than intended behavior. – Oleg Valter is with Ukraine Jul 20 '21 at 16:57
22

With TypeScript 4.4, the language gained support for more complex index signatures.

interface FancyIndices {
  [x: symbol]: number;
  [x: `data-${string}`]: string
}

The symbol key can be trivially caught by adding a case for it in the previously posted type, but this style of check cannot detect infinite template literals.1

However, we can achieve the same goal by modifying the check to see if an object constructed with each key is assignable to an empty object. This works because "real" keys will require that the object constructed with Record<K, 1> have a property, and will therefore not be assignable, while keys which are index signatures will result in a type which may contain only the empty object.

type RemoveIndex<T> = {
  [K in keyof T as {} extends Record<K, 1> ? never : K]: T[K]
}

Try it out in the playground

Test:

class X {
  [x: string]: any
  [x: number]: any
  [x: symbol]: any
  [x: `head-${string}`]: string
  [x: `${string}-tail`]: string
  [x: `head-${string}-tail`]: string
  [x: `${bigint}`]: string
  [x: `embedded-${number}`]: string

  normal = 123
  optional?: string
}

type RemoveIndex<T> = {
  [K in keyof T as {} extends Record<K, 1> ? never : K]: T[K]
}

type Result = RemoveIndex<X>
//   ^? - { normal: number, optional?: string  }

1 You can detect some infinite template literals by using a recursive type that processes one character at a time, but this doesn't work for long keys.

Gerrit0
  • 7,955
  • 3
  • 25
  • 32
  • Is there a way to make this work with primitive types like `string`. I tried to use `RemoveIndex` but the primitive type still has the string indexing constraints. – CMCDragonkai Oct 16 '21 at 07:21
  • @CMCDragonkai what are you actually trying to do? `RemoveIndex` (note capital `S`) gives you a type that looks like `string` without the index signatures, but that's probably not really useful... – Gerrit0 Oct 16 '21 at 20:33
  • Yea I needed to trick TS to think of the type as a primitive string, but without the index signature that `String` has. – CMCDragonkai Oct 18 '21 at 10:47
  • I've included this utility type as [`RemoveIndexSignature`](https://github.com/sindresorhus/type-fest/blob/main/source/remove-index-signature.d.ts) in [`type-fest`](https://github.com/sindresorhus/type-fest#readme) alongside an extensive comment explaining the mechanics. Thanks for sharing this great solution! – buschtoens Aug 18 '22 at 11:58
  • *confusion* that happened like 9 months ago, deja-vu answer discovery? – Gerrit0 Aug 19 '22 at 02:07
-2

There is not a really generic way for it, but if you know which properties you need, then you can use Pick:

interface Foo {
  [key: string]: any;
  bar(): void;
}

type FooWithOnlyBar = Pick<Foo, 'bar'>;

const abc: FooWithOnlyBar = { bar: () => { } }

abc.notexisting = 5; // error
jmattheis
  • 10,494
  • 11
  • 46
  • 58
  • 1
    Unfortunately this is for the AngularJS IScope interface. It has so many properties, it'd probably be easier to just copy/paste the type definition and modify it that way. – Laurence Dougal Myers Jul 24 '18 at 03:24
-3

Not really. You can't "subtract" something an interface like this one. Every member is public and anyone claiming to implement Foo, must implement them. In general, you can only extend interfaces, via extends or declaration merging, but not remove things from them.

Horia Coman
  • 8,681
  • 2
  • 23
  • 25
  • Thanks for your response, but I know it's certainly possible to derive types that exclude properties. I've been using an "Omit" type, "Pick>" (in TS 2.8), which lets you specify a base type and a union of string types. So, "type Baz = Omit;" gives a new type that only has the index signature. I'm curious to see if there's a similar way to omit the indexed type signature. – Laurence Dougal Myers Jul 22 '18 at 13:13
  • Oh, I see what you're asking. You're not interested in the two types being _compatible_, just that there's a nice way to type the thing without, I imagine, duplication. – Horia Coman Jul 22 '18 at 13:17