【问题标题】:Why can't Typescript infer the type of a nested generic function?为什么 Typescript 不能推断嵌套泛型函数的类型?
【发布时间】:2020-11-23 19:43:15
【问题描述】:

Typescript 将下面的applyReducer 调用的state 参数键入为unknown。如果我用applyReducer<State> 明确指定类型,它会起作用,但为什么有必要这样做?看起来很清楚类型应该是State

(打字稿 v4.1.2)

reducerFlow.ts:

type UnaryReducer<S> = (state: S) => S

export const applyReducer = <S>(reducer: UnaryReducer<S>) =>
  (state: S) => reducer(state)

foo.ts

interface State { a: number, b: number }
const initialState: State = { a: 0, b: 0 }

// Why is the type of state unknown?
// Typescript can't infer that it is State?
const foo = applyReducer(
    state => ({ ...state, b: state.b + 1 })
)(initialState)

TS Playground

【问题讨论】:

  • 对你来说很清楚,但 TypeScript 只知道它需要一些东西并返回具有 b 属性的东西! initialState 的使用是applyReducer 的调用完成后,不会从中推断出类型。

标签: typescript


【解决方案1】:

回答你的问题:

如果我使用 applyReducer 显式指定类型,它会起作用,但为什么需要这样做?

问题在于这是一个柯里化函数,即(x) =&gt; (y) =&gt; z。因为类型参数S 位于第一个函数上,所以只要您调用该函数,它就会“实例化”(获取具体类型)。您可以通过查看下面的 fn 类型来了解这一点:

const fn = applyReducer((state) => ({ ...state, b: 2 }))
//                                    ^^^^^^^^ error (because you can't spread 'unknown')
//
// const fn: (state: unknown) => unknown

因为在参数(state) =&gt; ({ ...state, b: 2 }) 中没有关于S 应该变成什么的可用信息,所以打字稿默认为unknown。所以S 现在是未知的,无论你以后如何称呼fn,它都将保持未知。

解决此问题的一种方法是 - 正如您所提到的 - 显式提供类型参数:

const fna = applyReducer<State>((state) => ({ ...state, b: 2 }))

另一种方法是为打字稿提供一些信息,从中可以推断出S 的类型,例如state 参数的类型约束:

const fnb = applyReducer((state: State) => ({ ...state, b: 2 }))

【讨论】:

  • 感谢您的解释。这似乎是 Typescript 中的一个错误。 fn 确实没有足够的类型信息来推断 state 的类型,但 fn(...)(initialState) 在编译时具有 initialState 的类型。类似地,&lt;T&gt;(x: T) =&gt; x 不能自己推断出x 的类型,但可以在(&lt;T&gt;(x: T) =&gt; x)(10) 中,即x 可以在稍后的调用表达式中从参数提供者推断。它不能对柯里化函数做同样的事情。
【解决方案2】:

更新 这是我的答案:

例子:


type UnaryReducer = <S>(state: S) => S

interface ApplyReducer {
  <T extends UnaryReducer>(reducer: T): <S,>(state: ReturnType<T> & S) => ReturnType<T> & S;
}

export const applyReducer: ApplyReducer = (reducer) =>
  (state) => reducer(state)


interface State { a: number, b: number }
const initialState: State = { a: 0, b: 0 }


const bar = applyReducer(
  state => ({ ...state, b: 2, })
)(initialState)
bar // {b: number; } & State

const bar2 = applyReducer(
  state => ({ ...state, b: '2', }) 
)(initialState) // Error: b is not a string

const bar3 = applyReducer(
  state => ({ ...state, b: 2, c:'2' }) 
)(initialState) // Error: Property 'c' is missing in type 'State'
 
const bar4 = applyReducer(
  state => ({ ...state }) 
)(initialState) // Ok

const bar5 = applyReducer(
  state => ({ a: 0, b: 0 }) // Error: you should always return object wich is extended by State
)(initialState)

const bar6 = applyReducer(
  state => ({...state, a: 0, b: 0 }) // Ok
)(initialState)

我们应该直接为箭头函数定义泛型参数

type UnaryReducer = <S>(state: S) => S

我们应该以某种方式绑定initialState 参数和reducer 的ReturnType

interface ApplyReducer {
  <T extends UnaryReducer>(reducer: T): <S,>(state: ReturnType<T> & S) => ReturnType<T> & S;
}

这意味着reducer(回调)的state参数应该始终是返回类型的一部分。

也就是说,如果你尝试:

state => ({ a:0, b: 2, }) 

这行不通,但我认为没有意义

更新 我认为这是不可能的。考虑这个例子:

const foo = applyReducer(
  state => ({ ...state, b: state.b + 1 })
)

const result = foo((initialState))

您只能在foo 通话期间推断initialState。在foo 声明期间,TS 不会预先知道参数类型。

TS 从左到右推断参数。

【讨论】:

  • 这样就失去了类型安全性。例如,尝试返回({ ...state, b: '' })。这应该会给出一个错误,但不会。
  • @JeffreyWesterkamp 你是对的。我会努力解决的
  • 感谢您的回答!这在某些方面是一种改进,但不幸的是,这对我来说并不重要:能够推断出state 在reducer 定义中属于State 类型。我需要能够在函数中使用state,例如state =&gt; { ...state, b: state.b + 1 }。我已经更新了 OP。
  • @RaineRevere 请看一下这个问题:github.com/microsoft/TypeScript/issues/31146 和这个答案stackoverflow.com/questions/55655707/… 也许 smbd else 将能够回答它
  • @captain-yossarian 感谢您的参考!我会留意他们的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-03-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-05-06
  • 1970-01-01
相关资源
最近更新 更多