Завдання

Реалізувати 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] };

Посилання