redux 如何在Typescript中编写严格要求其参数的函数类型

yftpprvb  于 2023-03-12  发布在  TypeScript
关注(0)|答案(1)|浏览(156)

我遇到了combineReducers不够严格的问题,我不知道如何解决它:

interface Action {
  type: any;
}

type Reducer<S> = (state: S, action: Action) => S;

const reducer: Reducer<string> = (state: string, action: Action) => state;

const reducerCreator = (n: number): Reducer<string> => reducer;

interface ReducersMapObject {
  [key: string]: Reducer<any>;
}

const reducerMap: ReducersMapObject = {
  test: reducer,
  test2: reducerCreator
}

我认为reducerMap会抛出一个错误,因为reducerCreator不是一个reducer(它是一个接受字符串并返回reducer的函数),但TypeScript对此没有问题。
看来问题的根源是还原器本质上归结为any => any,因为functions with fewer parameters are assignable to functions that take more params
这意味着ReducersMapObject类型基本上就是{[key: string]: function}
有没有办法使Reducer类型更严格地要求两个参数,或者有没有其他方法来更好地确保ReducersMapObject实际上包含reducer函数?
如果您尝试复制,这些代码都在TypeScript playground中编译

2w2cym1i

2w2cym1i1#

问得好...在这个相当长的答案的结尾有两个可行的选择。你问了两个问题,我分别回答了每个。

问题1:

有没有一种方法可以使Reducer类型更严格地要求两个参数...
要做到这一点是很困难的,因为TypeScript函数中存在两个障碍。

障碍物1:丢弃函数参数

一个障碍,你已经注意到了,is documented here在标题“比较两个函数”下。它说“我们允许'丢弃'参数。”也就是说,参数较少的函数可以赋值给参数较多的函数。基本原理在FAQ中。简而言之,下面的赋值是安全的,因为参数较少的函数“可以安全地忽略额外的参数。”

const add: (x: number, y: number) = 
           (x: number) => { return x; }; // works and is safe

障碍2:函数参数双方差

第二个障碍是函数参数是双变量的,这意味着我们不能用用户定义的类型参数来解决这个问题,在某些语言中,我们可以定义Pair沿着一个接受Pair的函数。

class Pair {
    x: number;
    y: number;
}

let addPair: (p: Pair) => number;

在具有协变函数的语言中,上述方法会将参数限制为Pair的子类型。
在TypeScript中,简单类型赋值遵循预期的可替换性规则,但函数遵循二元规则。在其简单类型赋值中,TypeScript允许我们将类型Pair赋值给类型Single,但不允许将类型Single赋值给类型Pair。这是预期的替换。

class Single {
    x: number;
}

let s: Single = new Pair(); // works
let p: Pair = new Single(); // fails because of a missing property.

然而,TypeScripts函数是二元的,并且不受相同的限制。

let addSingle: (s: Single) => number; 
addSingle = (p: Pair) => p.x + p.y; // as expected, Pair is assignable to Single.

let addPair: (p: Pair) => number;
addPair = (s: Single) => s.x; // surprise, Single is assignable to Pair!

结果是期望Pair的函数将接受Single

Reducers的影响

以下两种技术都不会强制Reducer实现必须接受的参数(或类属性)的数量。

class Action { }

// no restriction - TypeScript allows discarding function parameters 
type Reducer01<S> = (state: S, action: Action) => S;
const reducer01: Reducer01<number> = (state: number) => 0; // works

// no restriction - TypeScript functions have parameter bivariance
class ReducerArgs<S> { 
    state: S;
    action: Action;
}
type Reducer02<S> = (args: ReducerArgs<S>) => S;
const reducer02 = (args: { state: number }) => 0; // works

这实际上可能不是问题,因为让ReducersMapObject接受一个参数较少的Reducer是安全的。编译器仍然会确保:
1.对Reducer的每个调用包括 * 所有 * Reducer参数,并且

  1. X1 M15 N1 X的每个实现仅对它的(可能短的)参数列表进行操作。

问题2:

...还是另一种方法来更好地确保ReducersMapObject实际包含reducer函数?
我们尝试做的一件事是使reducerCreator函数(以及其他不常见的函数)与Reducer<S>函数类型不兼容。

可行选项1:自定义参数类型

上面的第二种技术,使用一个名为ReducerArgs<S>的用户定义类型,会给予我们带来更多的信心,但它不会提供完全的信心,因为我们仍然会有双方差,但它会确保编译器拒绝reducerCreator

interface Action {
  type: any;
}

// define an interface as the parameter for a Reducer<S> function
interface ReducerArgs<S> { 
    state: S;
    action: Action
}

type Reducer<S> = (args: ReducerArgs<S>) => S;

const reducer: Reducer<string> = (args: ReducerArgs<string>) => args.state;

const reducerCreator = (n: number): Reducer<string> => reducer;

interface ReducersMapObject {
  [key: string]: Reducer<any>;
}

const reducerMap: ReducersMapObject = {
  test: reducer,
  test2: reducerCreator // error!
}

可行选项2:泛型和联合类型

另一种选择是使用如下所示的通用ReducerMapObject<T>

interface ReducersMapObject<T> {
  [key: string]: Reducer<T>;
}

然后用a union type将其参数化,a union type列出了所有reducer的类型。

const reducer: Reducer<string> = (state: string, action: Action) => state;
const reducer1: Reducer<number> = (state: number, action: Action) => state;

const reducerMap: ReducersMapObject<string | number> = {
    test: reducer,
    test1: reducer1,
    test2: reducerCreator // error!
}

结果将是any => any变成T => T,其中T是联合体中列出的类型之一(顺便说一句,如果有一个类型说“x可以是任何类型,只要它是与y相同的类型”,那就太好了)。
虽然以上两种方法都需要更多的代码,而且有点笨拙,但它们确实满足了您的需求。这是一个有趣的研究项目。谢谢您的提问!

相关问题