Dlaczego Typescript nie może wywnioskować typu zagnieżdżonej funkcji ogólnej?

Nov 24 2020

Maszynopis wpisuje stateargument do applyReducerponiższego wywołania jako unknown. Działa, jeśli wyraźnie określę typ za pomocą applyReducer<State>, ale dlaczego jest to konieczne? Wydaje się całkiem jasne, jaki powinien być typ State.

(Typescript, wersja 4.1.2)

reduktorFlow.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

Odpowiedzi

JeffreyWesterkamp Nov 24 2020 at 03:56

Odpowiedzieć na Twoje pytanie:

Działa, jeśli jawnie określę typ za pomocą ApplyReducer, ale dlaczego jest to konieczne?

Problem polega na tym, że jest to funkcja curry, tj (x) => (y) => z. Ponieważ parametr typu Sznajduje się na pierwszej funkcji, będzie on „tworzyć instancję” (pobierać konkretny typ) zaraz po wywołaniu tej funkcji. Możesz to zobaczyć w akcji, patrząc na fnponiższe typy :

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

Ponieważ w argumencie (state) => ({ ...state, b: 2 })nie ma dostępnych informacji o tym, co Spowinno się stać, maszynopis domyślnie unknown. Więc Sjest teraz nieznane i niezależnie od tego, fnz czym później zadzwonisz , pozostanie nieznane.

Jednym ze sposobów rozwiązania tego problemu jest - jak wspomniałeś - jawne podanie argumentu type:

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

Innym sposobem jest podanie maszynopisu pewnych informacji, z których może wywnioskować typ S, na przykład ograniczenie typu dla stateparametru:

const fnb = applyReducer((state: State) => ({ ...state, b: 2 }))
captain-yossarian Nov 24 2020 at 03:07

UPDATE Oto moja odpowiedź:

Przykład:


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)

Powinniśmy zdefiniować ogólny parametr bezpośrednio dla funkcji strzałkowej

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

Powinniśmy jakoś powiązać initialStateargument i ReturnType reduktora

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

Oznacza to, że stateargument reduktora (callback) powinien być zawsze częścią typu zwracanego.

To znaczy, że jeśli spróbujesz:

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

to nie zadziała, ale myślę, że nie ma sensu tego robić