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

How do I write a Function type that strictly requires its parameters in Typescript

我 运行 遇到了 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(它是一个接受字符串和 returns 一个 reducer 的函数),但是 TypeScript 对此没有问题。

问题的根源似乎是 Reducer 本质上归结为 any => any 因为 functions with fewer parameters are assignable to functions that take more params..

这意味着ReducersMapObject类型基本上就是{[key: string]: function}

有没有办法让 Reducer 类型更严格地要求这两个参数,或者有其他方法来更加确信 ReducersMapObject 实际上包含 reducer 函数?

如果您尝试复制

,此代码全部编译在TypeScript playground

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

问题 1:

Is there a way to make the Reducer type stricter about requiring both parameters...

由于 TypeScript 函数中的两个障碍,这将很难实现。

障碍 1:丢弃函数参数

一个障碍,您已经注意到了,is documented here under the heading "Comparing Two Functions." It says that "we allow 'discarding' parameters." That is, functions with fewer parameters are assignable to functions with more parameters. The rationale is in the FAQ。简而言之,以下赋值是安全的,因为参数较少的函数 "can safely ignore extra parameters."

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

障碍二:函数参数双方差

第二个障碍是function parameters are bivariant。那 意味着我们不能使用用户定义的类型参数来解决这个问题。在某些语言中,我们可以定义 Pair 以及接受 Pair 的函数。

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

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

在具有协变函数的语言中,以上将参数限制为 Pair.

的子类型

在 TypeScript 中,简单的类型分配遵循预期 substitutability rules,但函数遵循双变规则。在其简单的类型分配中,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.

不过,TypeScript 函数是双变的,并且不受相同的限制。

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 属性)。

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 个参数,以及
  2. Reducer 的每个实现仅对其(可能很短)参数列表进行操作。

问题 2:

...or another way to get more confidence that the ReducersMapObject actually contains reducer functions?

我们正在尝试做的一件事是使 reducerCreator 函数(以及其他形状异常的函数)与 Reducer<S> 函数类型不兼容。这里有两个可行的选择。

可行方案一:用户自定义参数类型

上面的第二种技术,使用名为 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 参数化它,列出所有减速器的类型。

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 can be any type, so long as it is that same type as y.")

虽然以上两个都涉及更多代码并且有点笨拙,但它们确实可以满足您的目的。这是一个有趣的研究项目。谢谢提问!