Typecheck createAction Помощник Redux

У меня есть тип Action, определенный следующим образом:

type Action =
    { type: 'DO_X' }
    |
    { type: 'DO_Y', payload: string }
    |
    { type: 'DO_Z', payload: number }

Это тип объединения, в котором каждый элемент является действительным действием.

Теперь я хотел бы создать функцию createAction, которая принимает type и возвращает новую функцию, которая принимает payload.

const doZ = createAction('DO_Z')
console.log(doZ(42)) // { type: 'DO_Z', payload: 42 }

Здесь моя текущая реализация:

const createAction = (type: Action['type']) =>
  (payload?: any) =>
    ({ type, payload })

Это typechecks type, как я хочу. Как я могу также typecheck payload? Я хочу, чтобы payload соответствовал типу правильного действия на основе type. Например, doZ должен завершиться неудачей при вызове с помощью string, потому что он payload говорит, что он принимает только number.

Ответ 1

Канонический ответ на этот вопрос зависит от вашего конкретного случая использования. Я собираюсь предположить, что вам нужно Action точно оценить тип, который вы написали; то есть объект type: "DO_X" не имеет свойства payload любого типа. Это означает, что createAction("DO_X") должна быть функцией от нулевых аргументов, а createAction("DO_Y") должна быть функцией одного аргумента string. Я также предполагаю, что вы хотите, чтобы любые параметры типа на createAction() были автоматически выведены, поэтому вам не нужно, например, указывать createAction<Blah>("DO_Z") для любого значения Blah. Если какое-либо из этих ограничений отменено, вы можете упростить решение для чего-то вроде @Arnavion.


TypeScript не любит отображать типы из значений свойств, но он счастлив сделать это из ключей свойств. Поэтому давайте построим тип Action таким образом, чтобы предоставить нам типы, которые компилятор может нам помочь. Сначала мы описываем полезную нагрузку для каждого типа действия следующим образом:

type ActionPayloads = {
  DO_Y: string;
  DO_Z: number;
}

Пусть также ввести любые типы Action без полезной нагрузки:

type PayloadlessActionTypes = "DO_X" | "DO_W";

(Я добавил тип 'DO_W', чтобы показать, как он работает, но вы можете удалить его).

Теперь мы наконец можем выразить Action:

type ActionMap = {[K in keyof ActionPayloads]: { type: K; payload: ActionPayloads[K] }} & {[K in PayloadlessActionTypes]: { type: K }};
type Action = ActionMap[keyof ActionMap];

Тип ActionMap - это объект, чьи ключи являются type каждого Action, а значениями являются соответствующие элементы объединения Action. Это пересечение Action с payload s, а Action без payload s. И Action - это только тип значения ActionMap. Убедитесь, что Action - это то, что вы ожидаете.

Мы можем использовать ActionMap, чтобы помочь нам ввести функцию createAction(). Вот он:

function createAction<T extends PayloadlessActionTypes>(type: T): () => ActionMap[T];
function createAction<T extends keyof ActionPayloads>(type: T): (payload: ActionPayloads[T]) => ActionMap[T];
function createAction(type: string) {
  return (payload?: any) => (typeof payload === 'undefined' ? { type } : { type, payload });
}

Это перегруженная функция с параметром типа T, соответствующим type Action, который вы создаете. В двух верхних объявлениях описываются два случая: Если T является type для Action без payload, возвращаемый тип - это функция с нулевым аргументом, возвращающая правильный тип Action. В противном случае это функция с одним аргументом, которая принимает правильный тип payload и возвращает правильный тип Action. Реализация (третья подпись и тело) похожа на вашу, за исключением того, что она не добавляет payload к результату, если не передано payload.


Все сделано! Мы видим, что он работает по желанию:

var x = createAction("DO_X")(); // x: { type: "DO_X"; }
var y = createAction("DO_Y")("foo"); // y: { type: "DO_Y"; payload: string; }
var z = createAction("DO_Z")(5); // z: { type: "DO_Z"; payload: number; }

createAction("DO_X")('foo'); // too many arguments
createAction("DO_X")(undefined); // still too many arguments
createAction("DO_Y")(5); // 5 is not a string
createAction("DO_Z")(); // too few arguments
createAction("DO_Z")(5, 5); // too many arguments

Вы можете увидеть все это в действии на TypeScript игровой площадке. Надеюсь, это сработает для вас. Удачи!

Ответ 2

Подробно, но это работает:

type XAction = { type: 'DO_X', payload: undefined }; 
type YAction = { type: 'DO_Y', payload: string }; 
type ZAction = { type: 'DO_Z', payload: number }; 

type Action = XAction | YAction | ZAction;

const createAction = <T extends Action>(type: T['type']) =>
    (payload: T['payload']) =>
        ({ type, payload });

// Do compile:

createAction<XAction>("DO_X")(undefined);
createAction<YAction>("DO_Y")("foo");
createAction<ZAction>("DO_Z")(5);

// Don't compile:

createAction<XAction>("DO_X")(5); // Expected `undefined`, got number
createAction<YAction>("DO_Y")(5); // Expected string, got number
createAction<ZAction>("DO_X")(5); // Expected `"DO_Z"`, got `"DO_X"`

Более простой способ (не заставляя параметр типа createAction):

type Action = { type: 'DO_X', payload: undefined } | { type: 'DO_Y', payload: string } | { type: 'DO_Z', payload: number };

createAction("DO_Y")("foo");

к сожалению позволяет компилировать createAction<YAction>("DO_Y")(5) и т.д., поскольку T всегда выводится как Action, и поэтому параметр payload string|number|undefined