redux 具有对象文字和Typescript的函数开关case

kcrjzv8t  于 2022-11-12  发布在  TypeScript
关注(0)|答案(2)|浏览(150)

所以我在一个todomvc中得到了这个经典的switch case redux reducer,我想让它功能化,但似乎不能让我的头围绕ts类型。
Switch case在模式匹配方面非常有效,并且可以通过类型来缩小动作区分的并集。但是我似乎不知道如何用函数方法来传递缩小的动作,在这种方法中,对象文字的键应该进行类型缩小。
到目前为止,我得到的是所有函数的联合类型和一些ts错误。我真的很感激在这件事上的任何帮助,以获得一个更好的想法如何使用严格类型与ts。

import { action as actionCreator } from 'typesafe-actions';
import uuid from 'uuid';

import { ITodo } from 'types/models';

const ADD_TODO = 'todos/ADD_TODO';
const TOGGLE_ALL = 'todos/TOGGLE_ALL';
const REMOVE_TODO = 'todos/REMOVE_TODO';

export const addTodo = (title: string) => actionCreator(ADD_TODO, { title });
export const removeTodo = (id: string) => actionCreator(REMOVE_TODO, { id });
export const toggleAll = (checked: boolean) =>
  actionCreator(TOGGLE_ALL, { checked });

type TodosAction =
  | ReturnType<typeof addTodo>
  | ReturnType<typeof removeTodo>
  | ReturnType<typeof toggleAll>;
type TodosState = ReadonlyArray<ITodo>;

// no idea what typings should be
const switchCase = <C>(cases: C) => <D extends (...args: any[]) => any>(
  defaultCase: D
) => <K extends keyof C>(key: K): C[K] | D => {
  return Object.prototype.hasOwnProperty(key) ? cases[key] : defaultCase;
};

export default function(
  state: TodosState = [],
  action: TodosAction
): TodosState {
  // union type of 4 functions
  const reducer = switchCase({
    // (parameter) payload: any
    // How do I get types for these?
    [ADD_TODO]: payload => [
      ...state,
      {
        completed: false,
        id: uuid.v4(),
        title: payload.title,
      },
    ],
    [REMOVE_TODO]: payload => state.filter(todo => todo.id !== payload.id),
    [TOGGLE_ALL]: payload =>
      state.map(todo => ({
        ...todo,
        completed: payload.checked,
      })),
  })(() => state)(action.type);

  // [ts] Cannot invoke an expression whose type lacks a call signature. Type
  // '((payload: any) => { completed: boolean; id: string; title: any; }[]) |
  // ((payload: any) => ITodo[...' has no compatible call signatures.
  return reducer(action.payload);
}
f87krz0w

f87krz0w1#

一个有趣的类型问题。第一个关于有效载荷类型的问题我们可以通过传入所有可能的操作(TodosAction)来解决,并要求switchCase的参数必须是一个Map类型,该类型将包含联合体中所有types的属性,对于每一个类型,我们可以使用Extract条件类型来提取有效载荷类型。
问题的第二部分是由以下事实引起的:当使用键对类型进行索引时,(即联合类型本身),您将从该类型中获得所有可能值的联合。在本例中,这将是函数的联合,该类型脚本不认为是可调用的。为了解决这个问题,我们可以更改内部函数的公共签名,以返回一个函数,该函数将所有有效负载的并集作为参数,而不是每个函数都有一个有效负载。
结果如下所示:

import { action as actionCreator } from 'typesafe-actions';
import * as uuid from 'uuid';

interface ITodo{
    id: string
}

const ADD_TODO = 'todos/ADD_TODO';
const TOGGLE_ALL = 'todos/TOGGLE_ALL';
const REMOVE_TODO = 'todos/REMOVE_TODO';

export const addTodo = (title: string) => actionCreator(ADD_TODO, { title });
export const removeTodo = (id: string) => actionCreator(REMOVE_TODO, { id });
export const toggleAll = (checked: boolean) =>
    actionCreator(TOGGLE_ALL, { checked });

type TodosAction =
    | ReturnType<typeof addTodo>
    | ReturnType<typeof removeTodo>
    | ReturnType<typeof toggleAll>;
type TodosState = ReadonlyArray<ITodo>;

type Payload<TAll extends { type: any; payload: any }, P> = Extract<TAll, { type: P}>['payload']
type Casses<T extends { type: any; payload: any }, TState> = { [P in T['type']]: (payload: Payload<T, P>) => TState }

const switchCase = <C extends { type: any; payload: any }, TState>(cases: Casses<C, TState>) => 
    <D extends (payload: any) => TState >(defaultCase: D) => {
        function getCase <K extends string>(key: K): (arg: Payload<C, K>) => TState
        function getCase <K extends string>(key: K): Casses<C, TState>[K] | D {
            return cases.hasOwnProperty(key) ? cases[key] : defaultCase;
        }
        return getCase;
    };

export default function (
    state: TodosState = [],
    action: TodosAction
): TodosState {
    // union type of 4 functions
    const reducer = switchCase<TodosAction, TodosState>({
        [ADD_TODO]: payload => [
            ...state,
            {
                completed: false,
                id: uuid.v4(),
                title: payload.title,
            },
        ],
        [REMOVE_TODO]: payload => state.filter(todo => todo.id !== payload.id),
        [TOGGLE_ALL]: payload =>
            state.map(todo => ({
                ...todo,
                completed: payload.checked,
            })),
    })(() => state)(action.type);

    return reducer(action.payload);
}

Playground链接

yeotifhr

yeotifhr2#

这并不是对最初问题的准确回答,但它是相邻的,可能会帮助一些人在谷歌上遇到这个问题。
我正在尝试将useReducer与TypeScript一起使用。以下是我的设置:

interface State {
  loading: boolean;
  // blah blah
}

type Action = ReturnType<typeof setLoading | typeof someOtherAction | type anotherAction>;

// action creator
const setLoading = (value: boolean) => ({
  type: "setLoading" as const,
  payload: value,
});

// some other action creator

const reducer = (state: State, action: Action) => {
  switch (action.type) {
    case "setLoading":
      return { ...state, loading: action.payload };

    // other actions

    default:
      return state;
  }
};

我希望所有的东西都是类型安全的,我希望有动作创建器函数,我不希望必须多次定义我的动作/负载类型。

对我来说,最重要的是as const。没有它,TypeScript会认为setLoading函数的返回类型是string,而不是字符串文字setLoading。我猜是因为对象是可变的吧?总之,as const会生成const setLoading: (value: boolean) => { type: "setLoading"; payload: boolean; }类型,这会使所有类型都正常工作。

相关问题