Typescript: выводит тип generi c после необязательного первого универсального - PullRequest
1 голос
/ 24 февраля 2020

У меня есть функция с двумя обобщенными c типами, In и Out:

function createTask<
  In extends Record<string, any> = {},
  Out extends Record<string, any>,
>(task : TaskFunction<In, Out>) : Task<In, Out>

type TaskFunction<In, Out> = (args : TaskWrapper<In>) => Out | Promise<Out>; 
// TaskWrapper wraps several other types and interfaces, so args is more than just `In`

Этот код в настоящее время не компилируется, поскольку вы не можете иметь требуемый универсальный c тип (Out) после необязательного (In).

Как мне сказать компилятору Typescript, что я хочу позволить пользователю этой функции сделать одну из трех вещей:

  1. Не указывайте никаких обобщений: createTask(...). Тип In должен по умолчанию принимать значение {}, а Out должно выводиться из возвращаемого значения TaskFunction.

  2. Указать только In: createTask<A>(...). Как и выше, Out должно быть выведено.

  3. Укажите In и Out: createTask<A, B>(...).

По существу I ищу способ сказать "этот generi c является необязательным и должен быть выведен". Я знаю, что есть ключевое слово infer, но из ограниченной документации, которую я нашел, оно не поддерживает этот вариант использования.

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

Я могу изменить порядок In и Out, но тогда всегда нужно указывать Out, даже если его легко вывести, если пользователь хочет указать In.

Я также предпочитаю не заставлять пользователей добавлять значение по умолчанию {} каждый раз, когда они вызывают функцию.

Это вообще возможно сделать с Typescript, или мне всегда нужно будет указывать In?

Ответы [ 2 ]

2 голосов
/ 24 февраля 2020

Требуется что-то вроде частичного вывода параметров типа, который в настоящее время не является функцией TypeScript (см. microsoft / TypeScript # 26242 ). Прямо сейчас вы должны указать все параметры типа вручную или позволить компилятору определить все параметры типа; нет частичного вывода. Как вы заметили, generi c параметр типа по умолчанию do not scratch this зуд; по умолчанию отключается логический вывод.

Таким образом, существуют обходные пути. Те, которые работают последовательно, но также несколько раздражают в использовании, это либо curry , либо "dummying". Выделение карри означает, что вы разделяете одну функцию с несколькими типами аргументов на несколько функций с одним типом аргумента:

type Obj = Record<string, any>; // save keystrokes later

declare const createTaskCurry:
    <I extends Obj = {}>() => <O extends Obj>(t: TaskFunction<I, O>) => Task<I, O>;

createTaskCurry()(a => ({ foo: "" }));
// I is {}, O is {foo: string}
createTaskCurry<{ bar: number }>()(a => ({ foo: "" }));
// I is {bar: number}, O is {foo: string}
createTaskCurry<{ bar: number }>()<{ foo: string, baz?: number }>(a => ({ foo: "" }));
// I is {bar: number}, O is {foo: string, baz?: number}

У вас есть точное поведение, которое вы хотите по отношению к типам I и O , но в этом есть раздражающий отложенный вызов функции.


Подстановка здесь означает, что вы задаете для функции фиктивный параметр типов, которые вы хотите указать вручную, и позволяете выводу заменять ручная спецификация:

declare const createTaskDummy:
    <O extends Obj, I extends Obj = {}>(t: TaskFunction<I, O & {}>, 
      i?: I, o?: O) => Task<I, O>;

createTaskDummy(a => ({ foo: "" }));
// I is {}, O is {foo: string}
createTaskDummy(a => ({ foo: "" }), null! as { bar: number });
// I is {bar: number}, O is {foo: string}
createTaskDummy(a => ({ foo: "" }), null! as { bar: number }, 
  null! as { foo: string, baz?: number });
// I is {bar: number}, O is {foo: string, baz?: number}

Снова, у вас есть поведение, которое вы хотите, но вы передаете бессмысленные / фиктивные значения функции.

Конечно, если у вас уже есть параметры правильные типы, вам не нужно добавлять «фиктивный» параметр. В вашем случае вы, безусловно, можете предоставить в параметре task достаточно информации, чтобы компилятор мог вывести I и O, пометив или указав другие типы в параметре task:

declare const createTaskAnnotate: 
  <O extends Obj, I extends Obj = {}>(t: TaskFunction<I, O>) => Task<I, O>;

createTaskAnnotate(a => ({ foo: "" }));
// I is {}, O is {foo: string}
createTaskAnnotate((a: { bar: number }) => ({ foo: "" }));
// I is {bar: number}, O is {foo: string}
createTaskAnnotate((a: { bar: number }): { foo: string, baz?: number } => ({ foo: "" }));
// I is {bar: number}, O is {foo: string, baz?: number}

Это, вероятно, решение, которое я бы порекомендовал здесь, и оно фактически совпадает с другим ответом, опубликованным . Таким образом, весь этот ответ кропотливо объясняет, почему то, что вы хотите сделать в настоящее время, невозможно и почему доступные обходные пути уводят вас от этого. Ну хорошо!


Хорошо, надеюсь, это поможет разобраться в ситуации. Удачи!

Детская площадка ссылка на код

1 голос
/ 24 февраля 2020

Сначала полностью удалите тип по умолчанию:

declare function createTask<
    In extends Record<string, any>,
    Out extends Record<string, any>,
    >(task: TaskFunction<In, Out>): Task<In, Out>;

Для описываемого вами случая, где передается In:

const r1 = createTask<{ a : number }>(arg => {
    return { b: arg.a };
}); // Error: Expected 2 type arguments, but got 1.

Не передавайте его в качестве параметра типа. Аннотируйте значение, которое вы хотите ограничить, и позвольте ему выводить остальные типы:

const r1 = createTask((arg: { a: number }) => {
    return { b: arg.a };
}); // r1 is Task<{a: number;}, {b: number;}>

Это также работает, когда все типы известны:

declare function foo(arg: { a: number }): { b: boolean };

const r1 = createTask(foo); // r1 is Task<{a: number;}, { b: boolean;}>

Я пытался добавив TaskWrapper, как вы указали в ваших правках. Решение кажется идентичным.

type Task<In, Out> = { a: In, b: Out}
type TaskWrapper<T> = { a: T }
type TaskFunction<In, Out> = (args : TaskWrapper<In>) => Out | Promise<Out>; 

declare function createTask<
  In extends Record<string, any>,
  Out extends Record<string, any>,
>(task : TaskFunction<In, Out>) : Task<In, Out>

const r1 = createTask((args: TaskWrapper<{ a: number }>) => {
    return { b: args.a.a };
}); // r1 is Task<{a: number;}, {b: number;}>
...