
Про Змейку
В начале 2022 года Змейка (Snake on TS) была ещё Snake on JS. Но прогресс не стоит на месте, и было принято решение, освоить TypeScript и избавить Змейку от any. Никаких сверхъестественных типов там нет, да и речь не о ней. Но поиграть можете :)
Не говорите, что это hard
В репозитории type-challenges каррирование находится в разделе hard,

но мне захотелось реализовать этот тип ещё до того как я это узнал. Начнём.
Для начала напишем саму функцию curry
function curry<Fn extends Func<any, any>>(func: Fn) { return function _curry(...args: Array<any>) { if (args.length === func.length) { return func(...args); } return function (...args2: Array<any>) { return _curry(...[...args, ...args2]); }; }; }
получилось вот это. Как результат: корми сколько угодно параметров и какие угодно.
Теперь приступим к типу Curry.
В первой итерации получаем:
type Curry< Fn extends (...args: Array<any>) => any, Params extends Parameters<Fn>[number][] = [] > = Fn extends (...args: infer FnParams) => infer Return ? Params['length'] extends FnParams['length'] ? Return : <Args extends Array<any>>(...args: Args) => [...Params, ...Args]['length'] extends FnParams['length'] ? Return : <Args2 extends Array<any>>(...args: Args2) => Curry<Fn, [...Args, ...Args2]> : never
На входе функция Fn, с которой мы и будем работать внутри типа.
Params - нужны для отслеживания аргументов функции, если они не были переданы все сразу.
Через infer получаем FnParams и Return нашей функции Fn - так удобнее потом будет работать с ними.
Делаем проверку равенства количества Params и FnParams, если равны "делаем" Return.
Если не равны, реализуем curry в типовом варианте. Возвращаем функцию, в которой проверяем длину Args, если длина равна длине аргументов Fn, то возвращаем Return, если нет - возвращаем функцию, которая принимает оставшиеся аргументы.
Применим наш тип к функции curry
function curry<Fn extends (...args: Array<any>) => any>(func: Fn) { return function _curry(...args: Array<any>) { if (args.length === func.length) { return func(...args); } return function (...args2: Array<any>) { return _curry(...[...args, ...args2]); }; } as Curry<Fn>; }

Вроде всё хорошо и finalSum - это number, но...

Что-то пошло не так. Продолжим наши поиски.
Нужно связать параметры нашей Fn с Args и Args2. Этим мы и займёмся.
type Curry< Fn extends (...args: Array<any>) => any, Params extends Parameters<Fn>[number][] = [] > = Fn extends (...args: infer FnParams) => infer Return ? Params['length'] extends FnParams['length'] ? Return : <Args extends ParamsSlice<FnParams, Params>>(...args: Args) => [...Params, ...Args]['length'] extends FnParams['length'] ? Return : <Args2 extends ParamsSlice<FnParams, [...Params, ...Args]>>(...args: Args2) => Curry<Fn, [...Params , ...Args, ...Args2]> : never
Написали вспомогательный тип ParamsSlice
type ParamsSlice< FnParams extends Array<any>, Args extends FnParams[number][] > = FnParams extends [...Args, ...infer Rest] ? Rest extends [infer First, ...infer P] ? [First, ...Partial<P>] : [] : []
Здесь мы берём оставшиеся аргументы переданной функции и говорим, что первый, из оставшихся, будет обязательным, остальные опциональные.

У нас есть проверка на типы, на количество параметров и, даже, currySum с двумя аргументами возвращает number.
Это однозначно успех. Но сколько аргументов принимает firstNumValid? Давайте проверим.

Второй аргумент sum потерялся. Будем искать.
Args2 extends ParamsSlice<FnParams, [...Params, ...Args]>
Из ParamsSlice возвращается пустой массив. Выясним почему так происходит?
type ParamsSlice< FnParams extends Array<any>, Args extends FnParams[number][] > = FnParams extends [...Args, ...infer Rest] ? Rest extends [infer First, ...infer P] ? [First, ...Partial<P>] : [] : [FnParams, Args]
Для отладки вернём из ParamsSlice переданные типы: FnParams и Args.

А теперь проверим как работает
FnParams extends [...Args, ...infer Rest]
В нашем случае, мы проверяем a extends 4 , где a - number и результат отрицательный. Проверим это, написав простой тип:

Вывод: нужно приводить наши Args из ParamsSlice к примитивам. То есть сделаем из 4 -number.
type ToPrimitive<T> = 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<Rest, [...Res, ToPrimitive<First>]> : never : never
Просто проходим по всему массиву и применяем к каждому элементу ToPrimitive. Проверяем.

Поправим наш тип ParamsSlice, добавив MapPrimitive:
type ParamsSlice< FnParams extends Array<any>, Args extends FnParams[number][] > = FnParams extends [...MapPrimitive<Args>, ...infer Rest] ? Rest extends [infer First, ...infer P] ? [First, ...Partial<P>] : [] : []
Посмотрим на нашу функцию firstNumValid

Функция ожидает один аргумент с типом number и возвращает number. Работает!
Повторим эксперимент, который проводили чуть ранее:

Ну и ещё немного тестов

Тут весь код
type ToPrimitive<T> = 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<Rest, [...Res, ToPrimitive<First>]> : never : never type ParamsSlice< FnParams extends Array<any>, Args extends FnParams[number][] > = FnParams extends [...MapPrimitive<Args>, ...infer Rest] ? Rest extends [infer First, ...infer P] ? [First, ...Partial<P>] : [] : [] type Curry< Fn extends (...args: Array<any>) => any, Params extends Parameters<Fn>[number][] = [] > = Fn extends (...args: infer FnParams) => infer Return ? Params['length'] extends FnParams['length'] ? Return : <Args extends ParamsSlice<FnParams, Params>>(...args: Args) => [...Params, ...Args]['length'] extends FnParams['length'] ? Return : <Args2 extends ParamsSlice<FnParams, [...Params, ...Args]>>(...args: Args2) => Curry<Fn, [...Params , ...Args, ...Args2]> : never