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

Posted

技术标签:

【中文标题】如何在 Typescript 中编写严格要求其参数的函数类型【英文标题】:How do I write a Function type that strictly requires its parameters in Typescript 【发布时间】:2018-02-04 13:05:07 【问题描述】:

我遇到了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 可以做到这一点。

似乎问题的根源在于Reducer本质上归结为any =&gt; any,因为functions with fewer parameters are assignable to functions that take more params..

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

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

如果您尝试复制,此代码将全部在 TypeScript playground 中编译

【问题讨论】:

如果将所有“any”替换为 T 类型会得到什么? @glenn 将 reducersMapObject 中的 替换为 T 将要求 map 中的所有 reducer 返回相同类型的状态。这些 reducer 应管理不同的状态块因此需要能够返回他们各自负责的任何类型 好点。带有字段“state:any”的新类型/类“ReducerState”怎么样。就像您的状态信息的类型包装器一样。在你的 Code Reducer 中有这样的东西。我不确定这是否有帮助?但这就像 EventArg 或 Dto 行为。 @glenn 不确定我是否关注.. 你能发布一个代码示例来说明你的建议吗? 【参考方案1】:

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

问题一:

有没有办法让 Reducer 类型更严格地要求两个参数...

这将很难实现,因为 TypeScript 函数存在两个障碍。

障碍1:丢弃函数参数

您已经注意到一个障碍,is documented here 在标题“比较两个函数”下。它说“我们允许'丢弃'参数。”也就是说,参数较少的函数可以分配给参数较多的函数。 rationale is in the FAQ。简而言之,下面的赋值是安全的,因为参数较少的函数“可以安全地忽略多余的参数”。

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

障碍2:函数参数双方差

第二个障碍是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.

不过,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 是安全的。编译器仍将确保:

    Reducer 的每次调用都包含所有 Reducer 参数,以及 Reducer 的每个实现仅对其(可能很短)参数列表进行操作。

问题2:

...或者另一种方法来让 ReducersMapObject 实际上包含 reducer 函数更有信心?

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

可行的选项1:用户定义的参数类型

上面的第二种技术,使用称为ReducerArgs&lt;S&gt; 的用户定义类型,将给我们更多信心。它不会提供完全的信心,因为我们仍然会有双变量,但它会确保编译器拒绝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&lt;T&gt;,如下所示:

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

然后用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 =&gt; any 变为T =&gt; T,其中T 是联合中列出的类型之一。 (顺便说一句,最好有一个类型说,“x 可以是任何类型,只要它与 y 的类型相同。”)

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

【讨论】:

以上是关于如何在 Typescript 中编写严格要求其参数的函数类型的主要内容,如果未能解决你的问题,请参考以下文章

降级使用 Typescript 编写的 angular2 组件。如何将其导入用 es5 编写的 angular 1 项目?

如何使用/声明一个不会在 Angular 11 中传递严格和 noImplicitReturns TypeScript 设置的外部模块?

如何在 TypeScript 3+ 中正确键入通用元组剩余参数?

TypeScript 具有相同参数的多种返回类型

typescript使用小结

如何将参数从 Dart 传递到用 Typescript 编写的 Cloud 函数,它还给了我错误代码 UNAUTHENTICATED