Типизируй с нами, типизируй, как мы…
Обои рабочего стола)
Про Змейку
В начале 2022 года Змейка (Snake on TS) была ещё Snake on JS. Но прогресс не стоит на месте, и было принято решение, освоить TypeScript и избавить Змейку от any. Никаких сверхъестественных типов там нет, да и речь не о ней. Но поиграть можете:)
Не говорите, что это hard
В репозитории type-challengesкаррирование находится в разделе hard,
но мне захотелось реализовать этот тип ещё до того как я это узнал. Начнём.
Для начала напишем саму функцию curry
function curry>(func: Fn) {
return function _curry(...args: Array) {
if (args.length === func.length) {
return func(...args);
}
return function (...args2: Array) {
return _curry(...[...args, ...args2]);
};
};
}
получилось вот это. Как результат: корми сколько угодно параметров и какие угодно.
Теперь приступим к типу Curry.
В первой итерации получаем:
type Curry<
Fn extends (...args: Array) => any,
Params extends Parameters[number][] = []
> = Fn extends (...args: infer FnParams) => infer Return
? Params['length'] extends FnParams['length']
? Return
: >(...args: Args) => [...Params, ...Args]['length'] extends FnParams['length']
? Return
: >(...args: Args2) => Curry
: never
На входе функция Fn, с которой мы и будем работать внутри типа.
Params — нужны для отслеживания аргументов функции, если они не были переданы все сразу.
Через infer получаем FnParams и Return нашей функции Fn — так удобнее потом будет работать с ними.
Делаем проверку равенства количества Params и FnParams, если равны «делаем» Return.
Если не равны, реализуем curry в типовом варианте. Возвращаем функцию, в которой проверяем длину Args, если длина равна длине аргументов Fn, то возвращаем Return, если нет — возвращаем функцию, которая принимает оставшиеся аргументы.
Применим наш тип к функции curry
function curry) => any>(func: Fn) {
return function _curry(...args: Array) {
if (args.length === func.length) {
return func(...args);
}
return function (...args2: Array) {
return _curry(...[...args, ...args2]);
};
} as Curry;
}
Вроде всё хорошо и finalSum — это number, но…
Что-то пошло не так. Продолжим наши поиски.
Нужно связать параметры нашей Fn с Args и Args2. Этим мы и займёмся.
type Curry<
Fn extends (...args: Array) => any,
Params extends Parameters[number][] = []
> = Fn extends (...args: infer FnParams) => infer Return
? Params['length'] extends FnParams['length']
? Return
: >(...args: Args) => [...Params, ...Args]['length'] extends FnParams['length']
? Return
: >(...args: Args2) => Curry
: never
Написали вспомогательный тип ParamsSlice
type ParamsSlice<
FnParams extends Array,
Args extends FnParams[number][]
> = FnParams extends [...Args, ...infer Rest]
? Rest extends [infer First, ...infer P]
? [First, ...Partial]
: []
: []
Здесь мы берём оставшиеся аргументы переданной функции и говорим, что первый, из оставшихся, будет обязательным, остальные опциональные.
У нас есть проверка на типы, на количество параметров и, даже, currySum с двумя аргументами возвращает number.
Это однозначно успех. Но сколько аргументов принимает firstNumValid? Давайте проверим.
Второй аргумент sum потерялся. Будем искать.
Args2 extends ParamsSlice
Из ParamsSlice возвращается пустой массив. Выясним почему так происходит?
type ParamsSlice<
FnParams extends Array,
Args extends FnParams[number][]
> = FnParams extends [...Args, ...infer Rest]
? Rest extends [infer First, ...infer P]
? [First, ...Partial]
: []
: [FnParams, Args]
Для отладки вернём из ParamsSlice переданные типы: FnParams и Args.
А теперь проверим как работает
FnParams extends [...Args, ...infer Rest]
В нашем случае, мы проверяем a extends 4
, где a — number и результат отрицательный. Проверим это, написав простой тип:
Вывод: нужно приводить наши Args из ParamsSlice к примитивам. То есть сделаем из 4 -number.
type ToPrimitive = T extends number
? number
: T extends string
? string
: T extends boolean
? boolean
: T extends bigint
? bigint
: T extends symbol
? symbol
: {
[Key in keyof T]: T[Key];
};
Написали такой helper.
Проверяем.
Отлично. Теперь напишем тип MapPrimitive для наших Args:
type MapPrimitive<
Arr extends any[],
Res extends any[] = []
> = Arr extends []
? Res
: Arr extends [infer First, ...infer Rest]
? Rest extends any[]
? MapPrimitive]>
: never
: never
Просто проходим по всему массиву и применяем к каждому элементу ToPrimitive. Проверяем.
Работает)
Поправим наш тип ParamsSlice, добавив MapPrimitive:
type ParamsSlice<
FnParams extends Array,
Args extends FnParams[number][]
> = FnParams extends [...MapPrimitive, ...infer Rest]
? Rest extends [infer First, ...infer P]
? [First, ...Partial]
: []
: []
Посмотрим на нашу функцию firstNumValid
Функция ожидает один аргумент с типом number и возвращает number. Работает!
Повторим эксперимент, который проводили чуть ранее:
Проверка типов и количества аргументов
Ну и ещё немного тестов
Тут весь код
type ToPrimitive = T extends number
? number
: T extends string
? string
: T extends boolean
? boolean
: T extends bigint
? bigint
: T extends symbol
? symbol
: {
[Key in keyof T]: T[Key];
};
type MapPrimitive<
Arr extends any[],
Res extends any[] = []
> = Arr extends []
? Res
: Arr extends [infer First, ...infer Rest]
? Rest extends any[]
? MapPrimitive]>
: never
: never
type ParamsSlice<
FnParams extends Array,
Args extends FnParams[number][]
> = FnParams extends [...MapPrimitive, ...infer Rest]
? Rest extends [infer First, ...infer P]
? [First, ...Partial]
: []
: []
type Curry<
Fn extends (...args: Array) => any,
Params extends Parameters[number][] = []
> = Fn extends (...args: infer FnParams) => infer Return
? Params['length'] extends FnParams['length']
? Return
: >(...args: Args) => [...Params, ...Args]['length'] extends FnParams['length']
? Return
: >(...args: Args2) => Curry
: never