具有对象文字和Typescript的功能开关盒 [英] Functional switch case with object literal and Typescript

查看:62
本文介绍了具有对象文字和Typescript的功能开关盒的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

所以我在todomvc中安装了这个经典的开关盒redux减速器,我想使其功能化,但是似乎无法将ts的内容包起来.

So I've got this classic switch case redux reducer in a todomvc that I want to make functional but can't seem to wrap my head around ts typings for that.

开关盒非常适合模式匹配,并缩小按类型区分的联合动作.但是我似乎不知道如何使用一种功能化方法来传递缩小的动作,在该方法中对象文字的键应该进行类型缩小.

Switch case works great for pattern matching and narrows down action discriminated union by type. But I don't seem to get how to pass around narrowed actions with a functional approach where object literal's key should do a type narrowing.

到目前为止,我得到的是所有函数的联合类型以及一些ts错误.非常感谢您对此事有任何帮助,以便更好地了解如何在ts中使用严格类型.

What I got so far is union type of all functions and some ts errors by the way. Would really appreciate any help on the matter to get a better idea how to use strict types with 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);
}

推荐答案

一个有趣的打字问题.关于有效负载类型的第一个问题,我们可以通过传入所有可能的动作(TodosAction)来解决,并要求switchCase的参数必须是映射类型,该类型将包含联合中所有types的属性对于每种类型,我们可以使用Extract条件类型来提取有效载荷类型.

An interesting typing issue. The first problem regarding the payload types we can solve by passing in the all the possible actions (TodosAction), and requiring that the argument to switchCase must be a mapped type that will contain properties for all types in the union and for each type we can use the Extract conditional type to extract the payload type.

问题的第二部分是由于当您使用键索引类型(本身是并集类型)时,会从该类型中获得所有可能值的并集.在这种情况下,这将是函数的结合,而打字稿则认为它不是可调用的.为了解决这个问题,我们可以更改内部函数的公共签名,以返回一个将所有有效载荷的并集而不是每个都带有有效载荷的函数并集作为参数的函数.

The second part of the issue is cause by the fact that when you index into a type with a key (that is of a union type itself), you get a union of all possible values from the type. In this case that would be a union of functions, which typescript does not consider callable. To get around this we can change the public signature of the inner function to return a function that will take as an argument a union of all payloads instead of a union of functions that each take a payload.

结果看起来像这样:

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);
}

查看全文

登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆