Typescript выводит тип функции высшего порядка, которая выполняет необязательные параметры функции с такими же сигнатурами аргументов - PullRequest
0 голосов
/ 05 декабря 2018

Допустим, у меня есть функция, которая принимает две функции f и g в качестве аргументов и возвращает функцию, которая выполняет f и g и возвращает объект с результатами.Я также хочу обеспечить, чтобы f и g имели одинаковую подпись.Это достаточно просто с условными типами:

type ArgumentTypes<F extends Function> = F extends (...args: infer A) => any ? A : never;
function functionPair<
    F extends (...args: any[]) => any,
    G extends (...args: ArgumentTypes<F>) => any
>
(f: F, g: G): (...args: ArgumentTypes<F>) => { f: ReturnType<F>, g: ReturnType<G> }
{
    return (...args: ArgumentTypes<F>) => ({ f: f(...args), g: g(...args) });
}

functionPair((foo: string) => foo, (bar: number) => bar); // Error, incompatible signatures, as expected
functionPair((foo: string) => foo, (bar: string) => bar.length); // (foo: string) => { f: string; g: number; }, as expected

Теперь, , что если я хочу сделать f и g необязательными и иметь формувозвращаемого объекта изменить в результате?То есть, если f или g равно undefined, их ключ должен отсутствовать в результирующем объекте:

functionPair(); // Should be () => {}
functionPair(undefined, undefined); // Should be () => {}
functionPair((foo: string) => foo); // Should be (foo: string) => { f: string }
functionPair(undefined, (bar: string) => foo.length); // Should be (bar: string) => { g: number }
functionPair((foo: string) => foo, (bar: string) => foo.length); // Should be (foo: string) => { f: string, g: number }, as before

Я пытался выполнить это с помощью условных типов, но я 'У меня возникли проблемы с условным соблюдением формы результирующей функции.Вот что у меня есть (строгие нулевые проверки отключены):

function functionPair<
    A extends F extends undefined ? G extends undefined ? [] : ArgumentTypes<G> : ArgumentTypes<F>,
    F extends (...args: any[]) => any = undefined,
    G extends F extends undefined ? (...args: any[]) => any : (...args: ArgumentTypes<F>) => any = undefined
>
(f?: F, g?: G): (...args: A) =>
    F extends undefined
    ? G extends undefined ? {} : { g: ReturnType<G> }
    : G extends undefined ? { f: ReturnType<F> } : { f: ReturnType<F>, g: ReturnType<G> }
{ /* implementation... */ }

const a = functionPair(); // () => {}, as expected
const b = functionPair((foo: string) => foo); // (foo: string) => { f: string; }, as expected
const c = functionPair((foo: string) => foo, (bar: number) => bar); // Error, incompatible signatures, as expected
const d = functionPair((foo: string) => foo, (bar: string) => bar.length); // (foo: string) => { f: string; g: number; }, as expected

const e = functionPair(undefined, undefined); // INCORRECT! Expected () => {}, got (...args: unknown[] | []) => {} | { f: any; } | { g: any; } | { f: any; g: any; }
const f = functionPair(undefined, (bar: string) => bar.length); // INCORRECT! Expected (bar: string) => { g: number; } but got (...args: unknown[] | [string]) => { g: number; } | { f: any; g: number; }

Кстати, я знаю, что это технически возможно с перегрузками, как показано ниже, но я бы очень хотелпонять, как это сделать без них.

function functionPairOverloaded(): () => {}
function functionPairOverloaded(f: undefined, g: undefined): () => {}
function functionPairOverloaded<F extends (...args: any[]) => any>(f: F): (...args: ArgumentTypes<F>) => { f: ReturnType<F> }
function functionPairOverloaded<G extends (...args: any[]) => any>(f: undefined, g: G): (...args: ArgumentTypes<G>) => { g: ReturnType<G> }
function functionPairOverloaded<F extends (...args: any[]) => any, G extends (...args: ArgumentTypes<F>) => any>(f: F, g: G): (...args: ArgumentTypes<F>) => { f: ReturnType<F>, g: ReturnType<G> }
function functionPairOverloaded<F extends (...args: any[]) => any, G extends (...args: any[]) => any>(f?: F, g?: G) { /* implementation... */ }

1 Ответ

0 голосов
/ 05 декабря 2018

Если вы включили --strictNullChecks, думаю, я бы сделал это следующим образом:

type Fun = (...args: any[]) => any;
type FunFrom<F, G> = F extends Fun ? F : G extends Fun ? G : () => {};
type IfFun<F, T> = F extends Fun ? T : never;
type Ret<T> = T extends (...args: any[]) => infer R ? R : never

declare function functionPair<
  F extends Fun | undefined = undefined,
  G extends ((...args: (F extends Fun ? Parameters<F> : any[])) => any) 
    | undefined = undefined
>(
  f?: F, 
  g?: G
): (...args: Parameters<FunFrom<F, G>>) => {
  [K in IfFun<F, 'f'> | IfFun<G, 'g'>]: K extends 'f' ? Ret<F> : Ret<G> 
};

Это довольно уродливо, но дает вам поведение, которое вы ищете:

const a = functionPair(); // () => {}, as expected
const b = functionPair((foo: string) => foo); // (foo: string) => { f: string; }, as expected
const c = functionPair((foo: string) => foo, (bar: number) => bar); // Error, incompatible signatures, as expected
const d = functionPair((foo: string) => foo, (bar: string) => bar.length); // (foo: string) => { f: string; g: number; }, as expected
const e = functionPair(undefined, undefined); // () => {}, as expected
const f = functionPair(undefined, (bar: string) => bar.length); // (bar: string) => { g: number; }, as expected

Я решил использовать только два параметра типа F и G и вместо A использовать Parameters<FunFrom<F, G>>.Обратите внимание, что Parameters - это встроенная функция типа, аналогичная вашей ArgumentTypes.

Кроме того, для возвращаемого типа возвращаемой функции я делаю несколько уродливый отображенный тип.Сначала я планировал сделать что-то вроде IfFun<F, {f: Ret<F>}> & IfFun<G, {g: Ret<G>}>, что (я считаю) более понятно, но результирующий тип {f: X, g: Y} лучше, чем пересечение {f: X} & {g: Y}.

В любом случае, надеюсь, это поможет.Удачи!


Если вы хотите иметь возможность отключить --strictNullChecks, то определения становятся еще более привлекательными:

type Fun = (...args: any[]) => any;
type AsFun<F> = [F] extends [Fun] ? F : never
type FunFrom<F, G> = AsFun<IfFun<F, F, IfFun<G, G, () => {}>>>;
type IfFun<F, Y, N=never> = F extends undefined ? N : 
  0 extends (1 & F) ? N : F extends Fun ? Y : N;
type Ret<T> = T extends (...args: any[]) => infer R ? R : never

declare function functionPair<
  F extends Fun | undefined = undefined,
  G extends ((...args: IfFun<F, Parameters<F>, any[]>) => any)
  | undefined = undefined
  >(
    f?: F,
    g?: G
  ): (...args: Parameters<FunFrom<F, G>>) => {
    [K in IfFun<F, 'f'> | IfFun<G, 'g'>]: K extends 'f' ? Ret<F> : Ret<G>
  };

Разница в том, что IfFun<> необходимоуметь отличать функции от undefined и any, которые появляются в неудачных местах при выключении --strictNullChecks.Это потому, что undefined extends Function ? true : false начинает возвращать true, а any начинает выводиться, когда вы передаете ручное значение undefined в функции.Отличить undefined довольно просто, поскольку Function extends undefined ? true : false все еще false, но различение any раздражает и включает в себя забавный бизнес .

Удачи снова!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...