Typescriptがネストされたジェネリック関数のタイプを推測できないのはなぜですか?

Nov 24 2020

Typescriptは、以下stateapplyReducer呼び出しの引数をとして入力し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プレイグラウンド

回答

JeffreyWesterkamp Nov 24 2020 at 03:56

あなたの質問に答えるには:

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 }))
captain-yossarian Nov 24 2020 at 03:07

更新これが私の答えです:

例:


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, }) 

それはうまくいかないでしょうが、私はそれをする意味がないと思います