
Не так давно я раскопал на просторах GitHub репозиторий type-challenges. У меня есть целый блог, где я решаю задачки оттуда, но сегодня я попытаюсь показать не только реализацию Get, но и продемонстрирую общие проблемы, покажу улучшения и использование в production.
Если перед началом чтения хочется ознакомиться с понятиями из TypeScript, которые требуются в данной статье, переходите в конец.
Также, эта статья является переводом статьи, которую я написал на английском. Если интересно, переходите.
1. Базовая реализация
Текущий челлендж располагается в категории "сложное"
Предполагается, что нам нужно находить значения для пути только в объекте (реализация не требует пути в массиве и кортеже)
Так с чего же начнем?
1.1. Получение ключей
Представим, если бы мы решали эту задачу с помощью JavaScript:

Перед тем, как вызывать keys.reduce, мы получаем список всех ключей. В JavaScript нам достаточно вызвать метод split. В TypeScript нам тоже надо как-то получить список ключей из строки.
Благодаря TypeScript 4.1, мы можем использовать Template Literal types. С их помощью мы можем удалить точки между ключами. Давайте определим тип Path и попробуем сделать первый подход:

Выглядит коротко и просто. Однако после покрытия тестами мы поняли, что упустили случай с единственным элементом (без точки). Тесты написаны в Playground. Давайте добавим этот случай:

Так лучше. Тесты вместе с реализацией доступны в Playground.
1.2. Reducer для объекта
После того, как мы получили ключи, мы наконец-то может вызватьkeys.reduce. Чтобы это сделать, давайте определим тип GetWithArray , имея уже путь в виде кортежа K

Немного прокомментирую:
K extends [infer Key, ...infer Rest]проверяет, что у нас есть хотя бы один элемент в кортежеKey extends keyof Oпозволяет использоватьO[Key]и рекурсивно переходит к следующему уровню объекта
Давайте протестируем это решение (ссылка на Playground). Опять мы забыли случай, правда уже когда у нас пустой массив. После добавления код выглядит так:

Финальная версия с тестами в Playground
1.3. Все вместе

Давайте протестируем все вместе и удостоверимся, что тип работает как ожидается: Playground. Отлично, базовую часть мы закончили.
2. Опциональные пути
Когда работаешь с реальными данные в production, тебе иногда данные не приходят или приходят, но не полностью. Поэтому по всему проекту мы используем ? , null илиundefined.
Возьмем такой пример и покроем тестами текущее решение: Playground. Как и ожидалось, TypeScript ругается.
Причина проста. Давайте возьмем какой-нибудь пример и пошагово пройдемся:

Текущее решение не позволяет извлекать ключ из объекта, который может быть undefined or null. Постараемся это решить.
2.1. Удаляем undefined, null или оба типа сразу
Сначала определим 3 вспомогательных типа:

Мы проверяем, что undefined и/или null являются частью union type, и если так, удаляем их из него. Это поможет работать с остальной "существенной" частью.
Тесты, как обычно, в Playground
2.2. Редактируем reducer
Давайте обновим вот эту ветку GetWithArray:

Сначала проверим, что ключ существует в объекте с
undefinedи/илиnullВ противном случае, его нет (то есть мы возвращаем
undefined)

Добавим здесь тесты и проверим, что тип работает корректно (ссылка на Playground).
3. Получение пути из массива и кортежа
Аналогично берем пример с массивом и пошагово проверяем:

В JavaScript мы бы ходили по индексам:

Несмотря на то, что ключ может быть string или number, Path оставляем неизменным:

3.1. Reducer для массива
Как и для объектов, для массивов мы вызываем keys.reduce . Для TypeScript нам надо написать реализацию аналогично GetWithArray . Давайте реализуем это отдельно для массивов, а затем объединим реализации GetWithArray в одно.
Сперва адаптируем тип для массивов и кортежа. Возьмем A вместо O по семантическим причинам:

После тестирования в Playground, мы столкнулись с несколькими проблемами:
Массивы не имеют ключей с типом
string:
Здесь
'1' extends keyof string[]всегда ложно, поэтому возвращаетnever.Аналогично для массивов с ключевым словом
readonly
Кортежи (например
[0, 1, 2]) возвращаютneverвместоundefined:
Пойдем чинить все пошагово.
3.2. Выводим T | undefined

Для массивов мы хотим получить T | undefined в качестве ответа (так как при извлечении по индексу мы не знаем, есть элемент или нет), в зависимости от значения T:

Я добавил A extends readonly (infer T)[] , т.к. для всех массивов (в том числе с ключевым слово readonly) это утверждение верно.
После проверки, нам остается починить кортежи. Пример с тестами доступен в Playground.
3.3. Кортежи
Если мы попробуем извлечь значение из несуществующего индекса, мы получим обобщающий тип, как для массивов (ну и еще undefined в придачу)

Для того, чтобы справиться с этой проблемой, я предлагаю построить табличку с extends для разных типов (назовем эту табличку ExtendsTable) и будем подбирать правильное условие, чтобы разграничить массивы и кортежи:

Возьмем 4 разных типа:
[0]number[]readonly number[]any[]

Для лучшего отображения нарисую табличку, чтобы было понятно, что происходит:
|
|
|
| |
| ? | ? | ? | ? |
| ? | ? | ? | ? |
| ? | ? | ? | ? |
| ? | ? | ? | ? |
Если на пересечении ? , это значит, что строчка расширяема столбцом. Несколько примеров:
[0] extends [0]number[] extends readonly number[]
Соответственно, если на пересечении ?, то значит, что строка не расширяется колонкой. Пару примеров:
number[] extends [0]readonly number[] extends number[]
Возьмем строку с any[]: для колонки [0] мы видим ?, когда для остальных типов (столбцов) – это ?.
Собственно, мы нашли ответ!
Мы возьмем это условие any[] extends A и применим к GetWithArray:

Мы различаем массив от кортежа с помощью условия
any[] extends AДля массивов мы используем
T | undefinedДля кортежей, мы извлекаем значение, если индекс для этого кортежа существует
В противном случае, мы возвращаем
undefined
Если хочется еще раз взглянуть на все текущие изменения, переходите на Playground.
4. Общее решение
На данный момент у нас есть решение для объектов:

и для массивов:

Определим два вспомогательных типа: ExtractFromObject и ExtractFromArray, где мы будем извлекать значение, зная, с какой структурой в данный момент работаем:


Здесь пришлось добавлять ограничения (Generic Constrains):
Для
ExtractFromObject– этоO extends Record<PropertyKey, unknown>. Это значит, чтоOдолжен быть объектом любого видаДля
ExtractFromArrayаналогично:A extends readonly any[]принимает массив любого типа и кортежи
Добавим соответствующие условия в GetWithArray и объединим решения:

Это решение я тоже покрыл тестами. Ссылка на Playground.
5. Связка с JavaScript
Вернемся к решению в JavaScript:

На данный момент мы используем lodash в нашем проекте, где есть функция get. Если вы выглянете на common/object.d.ts в@types/lodash, то немного огорчитесь. Во многих случаях вызов get возвращает any : typescript-lodash-types
Давайте заменим reduce на цикл с for (например for-of), чтобы была возможность сделать ранний выход из цикла с полученным значением, если оно undefined или null:

А теперь покроем эту функцию get типами, которые мы получили на предыдущих шагах. Разделим это на два случая:
Тип
Getможно использовать тогда и только тогда, когда все ограничения применимы и тип корректно выводитсяВ случае какой-то ошибки мы используем вторую сигнатуру (например, мы передали число вместо строки в качестве пути)
Чтобы использовать перегрузку, нам нужно использовать функцию с ключевым слово function, а не стрелочные функции:

Почти готово. Осталось добавить тип Get :

Все вместе я разместил на Codesandbox:
Summary
Для решения задачи требуются следующие знания концепций в TypeScript:
Кортежи представлены в TypeScript 1.3, но вариативный вариант (Variadic Tuple Types) был выпущен в версии 4.0, так что теперь можно использовать spread внутри кортежей

Типы с условиями (Conditional types) доступны с версии TypeScript 2.8

Ключевое слово
inferв типах с условием, которые были представлены в TypeScript 2.8
Рекурсивные типы с условием (Recursive conditional types) появились с версии TypeScript 4.1

Шаблоны для строчных литералов (Template Literal types) также появились с версии TypeScript 4.1

Ограничения для дженериков (обобщений?) (Generic Constrains)

Перегрузка функций (Function Overloads)

Всем спасибо за внимание. Если есть пожелания, пишите в комментарии. Всем хорошего вечера и выходных.
Format-X22
Сурово.
Восхищает и пугает одновременно.
Перегрузка для фолбека особенно изящна.
BerAlIv Автор
Спасибо!
Постарался разжевать как можно лучше, но очень сложно читать такую большую статью. Надеюсь, summary как-то помог разобраться с базовыми концепциями TypeScript, т.к. язык развивается и добавляется много всего нового