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] };
Коментарі