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

от автора

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

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

Про Змейку

В начале 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 , где anumber и результат отрицательный. Проверим это, написав простой тип:

Вывод: нужно приводить наши Args из ParamsSlice к примитивам. То есть сделаем из 4number.

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 


ссылка на оригинал статьи https://habr.com/ru/post/721346/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *