Как стать автором
Обновить

Типизируй с нами, типизируй, как мы…

Уровень сложностиСредний
Время на прочтение4 мин
Количество просмотров6.2K
Обои рабочего стола)
Обои рабочего стола)

Про Змейку

В начале 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

Теги:
Хабы:
Всего голосов 10: ↑10 и ↓0+10
Комментарии2

Публикации

Ближайшие события