简体   繁体   中英

Why can't Typescript infer the type of a nested generic function?

Typescript types the state argument to the applyReducer call below as unknown . It works if I explicitly specify the type with applyReducer<State> , but why is this necessary? It seems pretty clear the type should be 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 Playground

UPDATE Here is my answer:

Example:


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)

We should define generic parameter directly for arrow function

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

We should somehow bind initialState argument and ReturnType of reducer

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

It is mean that state argument of reducer (callback) should be always a part of return type.

That's mean, that if you will try to:

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

it is not gonna work, but I think there is not sence to do it

To answer your question:

It works if I explicitly specify the type with applyReducer, but why is this necessary?

The problem is with the fact that this is a curried function, ie (x) => (y) => z . Because the type parameter S is on the first function it will 'instantiate' (get a concrete type) as soon as you call that function. You can see that in action by looking at the type of fn below:

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

Because in the argument (state) => ({ ...state, b: 2 }) there is no information available about what S should become, typescript defaults to unknown . So S is unknown now, and regardless of what you call fn with afterwards, it will stay unknown.

One way to fix this is to - as you mentioned - explicitly provide the type argument:

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

And another way is to give typescript some information from where it can infer the type for S , for example a type constraint on the state parameter:

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM