Comments 15
Ну не знаю... как по мне - открыли ящик Пандоры. Теперь логику размазываем и в коде и в типах.
Т.е. декомпозиция.
При правильном использовании делает код проще и надежнее.
При правильном использовании делает код проще и надежнее.
При правильном... Вот мы в конкретном примере видели, как простой массив action обрастает сложностью сразу и быстро. А ведь это просто /user/get-by-id - а мы так всё усложнили. Чего ради? Вместо 10 строк кода нам надо написать 150+ и помнить о наследовании, юнификации... А чтобы изменить-расширить - придётся делать наследование или юнион. Через год разобраться в этом будет нереально, а ведь именно для этого вроде всю историю с абстракциями затеяли.
А у нас просто страница. Просто обычное действие. Но мы сделали сложно, что развалится через 3 года,. Да-да... вы всё равно всё перепишите. Просто возьмите историю с 95 года - ничего не осталось.
Декомпозиция ли? Больше смахивает на сильное зацепление.
Зацепление может возникнуть, если перегибать с абстракциями и усложнять, где это не требуется. Но в этом и суть type-level подхода - он добавляет строгую типизацию там, где она реально нужна (например, в сложных API или конфигурациях), а не в простых сценариях. Для обычных действий никто не мешает использовать простые массивы или объекты. Главное - уметь вовремя остановиться и не применять тяжелую артиллерию к каждому кейсу 🙂
ИМХО, удачный пример использования type-level подхода — это работа с Redux Toolkit. Когда вы описываете endpoints, на основании их описания можно автоматически получить готовые типизированные хуки с различным функционалом (например, лениво мутирующие, простые GET-запросы и т.д.). Это помогает избавиться от рутины и избежать ошибок, сохраняя код чистым и понятным.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
// Описание API
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
createUser: builder.mutation<{ success: boolean }, { name: string; age: number }>({
query: (newUser) => ({
url: 'user',
method: 'POST',
body: newUser,
}),
}),
}),
});
// тут все типизировано :)
export const {
useCreateUserMutation, // Обычный хук
useLazyCreateUserMutation, // Ленивый хук
} = api;
function UserForm() {
const [createUser, { isLoading, isSuccess, isError }] = useCreateUserMutation();
const [triggerLazyCreateUser, { isLoading: isLazyLoading }] = useLazyCreateUserMutation();
const handleCreateUser = () => {
createUser({ name: 'John Doe', age: 30 });
};
const handleLazyCreateUser = () => {
triggerLazyCreateUser({ name: 'Jane Doe', age: 25 });
};
return (
<div>
{/* ... */}
</div>
);
}
Спасибо за мнение!
Логику в типах стоит применять по необходимости, чтобы не превратить код в хаос). Это дополнительный инструмент, а не замена обычного кода.
Статья как будто бы обо всём и ни о чём при этом. Вместо вольного пересказа документации можно было просто ссылки на неё дать, там изложение вполне доступное и корректное.
Касательно "нововведений":
Рассмотрим несколько ключевых новшеств, появившихся в последних версиях TS.
В каких версиях?
TypeScript теперь умеет оптимизировать условные типы глубже, избегая избыточных вычислений при работе с большими union-типами. Это ускоряет компиляцию и снижает нагрузку на систему типов.
Когда добавили? При чём тут Simplify
? Сюда бы ссылку на блогпост или на пул реквест с пояснениями.
Ранее у TypeScript были ограничения на глубину рекурсии для условных типов. Теперь эти лимиты увеличены, что дает простор для еще более глубоких алгоритмов:
Когда раньше? Какие были ограничения? Какие стали? С чем это связано? Опять же, даже если не объяснять надо хотя бы ссылку на источник дать.
Этому нововведению кстати уже 4 год пошёл.
Variadic Tuple Types (Вариативные кортежи)
Теперь можно использовать keyof более гибко вместе с шаблонными литералами. Это упрощает создание типов, привязанных к строковым ключам.
Пример тем временем не содержит keyof
вообще. Я так и не понял о чём речь была.
Чтобы не взрывать компилятор слишком сложными рекурсивными типами, TypeScript позволяет явно задавать глубину вычислений.
Чтобы не "взрывать компилятор" там просто стоит строгое ограничение на глубину рекурсии. Ничего задавать он не позволяет.
В примере просто какая-то счётная логика, это никак не "нововведение в TypeScript".
Спасибо за детальный фидбек!👍
Я понимаю, что некоторые фичи TypeScript (та же вариативность кортежей с TS 4.0) давно не в статусе горячих новостей. Однако статья и не претендовала на роль "свежайшей сводки" - её идея в том, чтобы дать обзорный срез тем, кто ещё не имел возможности глубоко разобраться в type-level приёмах.
Что касается ссылок на конкретные версии и документацию - да, при желании можно было бы детальнее расписать, в каких именно релизах появились те или иные возможности, или дать ссылки на соответствующие pull request’ы и блоги TypeScript. Но основная цель статьи - поделиться практическими кейсами и паттернами, а не быть исчерпывающей технической справкой.
В любом случае, замечания учту. Поправлю формулировки на которые вы указали, добавлю несколько уточнений и ссылок, чтобы новички или те, кто захочет глубже копнуть, могли быстро сориентироваться в официальной документации.
Спасибо, что обратили внимание на эти моменты!
Последний пример слегка переусложнен. В таких кейсах проще не менять число, а добавить вспомогательный кортеж:
type LimitDepth<T, Depth extends number, C extends 1[] = []> =
Depth extends C['length']
? T
: T extends (infer U)[]
? LimitDepth<U, Depth, [...C, 1]>
: T;
Хорошая статья. Тот, кто пытался минимизировать число невидимых связей к коде (особенно инфраструктурном, логических хардкодов), которые нужно "просто помнить" - тот поймет. Не раз этим занимался.
Цена у всего этого конечно не малая - резкое усложнение кода. Над поиском золотой середины между простым ЖСом и "абсолютной" типизацией ТСа и будет работать и спорить сообщество. Посмотрим к чему в итоге прийдем.
П.С. Для избежания путаницы, а бы указывал, что большая часть этих сложностей упадет на код инфры и фреймворка и их поддержку. Пользователи же (авторы клиентского кода и конкретных реализаций) получат же в основном +- те же интерфейсы, только с магической проверкой и т.д. Что бы не создавалось впечатления, что каждый мелкий метод РЕСТа на 10 строк заменится на кучу сложной дженерик магии.
Спасибо, что поделились мнением!
Cогласен: наиболее жесткие типовые конструкции обычно остаются в инфраструктуре и фреймворках, а конечные пользователи получают более удобные интерфейсы, без лишней магии в каждом методе. Вопрос поиска баланса между сложностью и безопасностью кода действительно ключевой, и здесь у каждого проекта своя "золотая середина".
Никогда не знал TypeScript, но примеры кода чем-то напомнили C++ :)
Меньше ошибок на этапе runtime. Многие проблемы ловятся компилятором.
Я загрузил код из статьи в TS Playground и чуть-чуть его поменял. В частности, в функцию createClient
я добавил отрицание перед route.hasIdParam
и TS его проглотила, не показала ни одной ошибки типов.
Другой пример
const routes: Routes = { ... } // вместо "as const satisfies Routes"
function example(routes: Routes) {
const apiClient = createClient(routes);
apiClient.getUsers().then(console.log);
apiClient.getUserById().then(console.log); // <- нет ошибки
if (routes.getUserById.hasIdParam)
apiClient.getUserById(123).then(console.log); // <- есть ошибка
apiClient.createUser().then(console.log);
}
Приведённый в статье пример вынесен в функцию, routes
имеет общий тип Routes
, что является типичной ситуацией, когда конфигурация не отлитая в золоте и не высеченная в граните ещё до компиляции, а, например, считывается из файла.
Почему-то getUserById
можно вызвать без аргументов, хотя она-то требует число. А если явно проверить, что hasIdParam истинно, то с числом вызвать нельзя.
Ложные-отрицательные срабатывания (проблемы нет, а ошибка случается) ещё терпимы, а ложно-положительные срабатывания (проблема в типизации есть, а компилятор молчит) — это бомба замедленного действия.
Можно ли где-то почитать конкретные критерии того, какие ошибки типизации отлавливаются TS, а какие требуют внимательного контроля со стороны программиста и тестирования?
Type-level программирование в TypeScript: практические кейсы и обзор возможностей