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

Обои рабочего стола)

Обои рабочего стола)

Про Змейку

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

Не говорите, что это hard

В репозитории type-challengesкаррирование находится в разделе hard,

42763972e7ef9b47b45095caf180d3ea.png

но мне захотелось реализовать этот тип ещё до того как я это узнал. Начнём.

Для начала напишем саму функцию 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;
}

cbcbcad89b8c160b8e347baf562ed104.png

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

f8a75606313d43795946a365f9b24748.png

Что-то пошло не так. Продолжим наши поиски.

Нужно связать параметры нашей 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

] : [] : []

Здесь мы берём оставшиеся аргументы переданной функции и говорим, что первый, из оставшихся, будет обязательным, остальные опциональные.

5dcbef588d0144e02ac9461f50e3414a.png

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

Это однозначно успех. Но сколько аргументов принимает firstNumValid? Давайте проверим.

607734604246acc80af05702eb1cb82c.png

Второй аргумент 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.

9523b41cbefa11e80311bafb5d8361da.png

А теперь проверим как работает

FnParams extends [...Args, ...infer Rest]

В нашем случае, мы проверяем a extends 4 , где a — number и результат отрицательный. Проверим это, написав простой тип:

23df2151192c9cdb73354754e4ce5f1a.png

Вывод: нужно приводить наши 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.
Проверяем.

fa9409c26f8023b55a62c5973af4fd12.png

Отлично. Теперь напишем тип 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

78367e7817946d47493d3a84a2a538cb.png

Функция ожидает один аргумент с типом number и возвращает number. Работает!

Повторим эксперимент, который проводили чуть ранее:

Проверка типов и количества аргументов

Проверка типов и количества аргументов

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

8c961bd342283b59d328d961fabb9847.pngТут весь код

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

© Habrahabr.ru