Проблема

Реализовать RemoveIndexSignature<T>, который исключает индексную сигнатуру с объектов. Например:

type Foo = {
  [key: string]: any;
  foo(): void;
};

type A = RemoveIndexSignature<Foo>; // expected { foo(): void }

Решение

Мы имеем дело с объектами в этом случае. Уверен, нам понадобятся сопоставляющие типы. Но, пока что, давайте разберемся в проблеме и что нам нужно сделать.

Нас попросили исключить индексные сигнатуры из объектных типов. Как эти сигнатуры выглядят? Используя оператор keyof, посмотрим, как TypeScript видит такие сигнатуры с точки зрения ключей объекта.

Например, имея тип “Bar”, на котором вызовем keyof, мы увидим следующую картину:

type Bar = { [key: number]: any; bar(): void }; // number | “bar”

Получается, что каждый ключ на объекте представлен как строковый тип литерал. В то же время, индексные сигнатуры представляются как общие типы, например number.

Это наводит меня на мысль, что мы можем отфильтровать и оставить только тип литералы. Но, как мы сравним или узнаем, является ли тип литералом или нет?

Воспользуемся тем, как ведут себя множества. Например, строчный литерал “foo” входит в множество строк, но строки не входят в множество “foo”. Потому что “foo” это множество из одного элемента и никак не покроет все строки.

"foo" extends string // true
string extends "foo" // false

Давайте это же свойство и применим в нашей проверке на литералы. Для начала, проверим случай со строками:

type TypeLiteralOnly<T> = string extends T ? never : never;

В случае, если T это string, условие выполнится с результатом true и мы вернём never. Почему? Потому что нам не нужны общие типы, нам нужны только литералы. Следовательно, мы пропускаем string. Такая же логика применима и к другому типу - number.

type TypeLiteralOnly<T> = string extends T
  ? never
  : number extends T
  ? never
  : never;

Что если T и не string и не number? Это значит что у нас сейчас тип литерал, который мы можем вернуть обратно как результат.

type TypeLiteralOnly<T> = string extends T
  ? never
  : number extends T
  ? never
  : T;

У нас на руках есть обёртка, которая возвращает нам только тип литерал и пропускает общий тип. Давайте применим это к каждому ключу объекта, используя сопоставляющие типы. Сделаем копию объекта, с которой будем работать:

type RemoveIndexSignature<T> = { [P in keyof T]: T[P] };

Во время итерации на ключах, мы можем поменять его тип, используя оператор as. Воспользуемся этим и добавим нашу обёртку:

type RemoveIndexSignature<T> = { [P in keyof T as TypeLiteralOnly<P>]: T[P] };

Таким образом, на каждой итерации, мы вызываем вспомогательный тип TypeLiteralOnly. Который, в свою очередь, возвращает переданный тип, если это литерал, и never, если индексная сигнатура.

type TypeLiteralOnly<T> = string extends T
  ? never
  : number extends T
  ? never
  : T;
type RemoveIndexSignature<T> = { [P in keyof T as TypeLiteralOnly<P>]: T[P] };

Что почитать