Remove Index Signature
Проблема
Реализовать 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] };
Комментарии