Typescriptがネストされたジェネリック関数のタイプを推測できないのはなぜですか?
Typescriptは、以下state
のapplyReducer
呼び出しの引数をとして入力しunknown
ます。でタイプを明示的に指定すると機能しますapplyReducer<State>
が、なぜこれが必要なのですか?タイプがである必要があることはかなり明らかなようState
です。
(Typescript 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プレイグラウンド
回答
あなたの質問に答えるには:
applyReducerでタイプを明示的に指定すると機能しますが、なぜこれが必要なのですか?
問題は、これがカリー化された関数であるという事実にあり(x) => (y) => z
ます。typeパラメータS
は最初の関数にあるため、その関数を呼び出すとすぐに「インスタンス化」(具体的な型を取得)します。fn
以下のタイプを見ると、実際に動作していることがわかります。
const fn = applyReducer((state) => ({ ...state, b: 2 }))
// ^^^^^^^^ error (because you can't spread 'unknown')
//
// const fn: (state: unknown) => unknown
引数(state) => ({ ...state, b: 2 })
には何にS
なるべきかについての情報がないため、typescriptのデフォルトはunknown。です。したがってS
、現在は不明でありfn
、後で何を呼び出すかに関係なく、不明のままになります。
これを修正する1つの方法は、前述のように、type引数を明示的に指定することです。
const fna = applyReducer<State>((state) => ({ ...state, b: 2 }))
また、別の方法は、typescriptに、タイプを推測できる情報を提供することS
です。たとえば、state
パラメーターのタイプ制約などです。
const fnb = applyReducer((state: State) => ({ ...state, b: 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
、レデューサーの引数と戻り値の型をバインドする必要があります
interface ApplyReducer {
<T extends UnaryReducer>(reducer: T): <S,>(state: ReturnType<T> & S) => ReturnType<T> & S;
}
これはstate
、reducer(コールバック)の引数が常に戻り値の型の一部であることを意味します。
つまり、次のことを試みた場合:
state => ({ a:0, b: 2, })
それはうまくいかないでしょうが、私はそれをする意味がないと思います