Warum kann Typescript nicht auf den Typ einer verschachtelten generischen Funktion schließen?

Nov 24 2020

Typescript gibt das stateArgument für den folgenden applyReducerAufruf als ein unknown. Es funktioniert, wenn ich den Typ explizit mit spezifiziere applyReducer<State>, aber warum ist das notwendig? Es scheint ziemlich klar, dass der Typ sein sollte State.

(Typoskript 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 Spielplatz

Antworten

JeffreyWesterkamp Nov 24 2020 at 03:56

Zur Beantwortung Ihrer Frage:

Es funktioniert, wenn ich den Typ explizit mit applyReducer spezifiziere, aber warum ist das notwendig?

Das Problem ist die Tatsache, dass dies eine Curry-Funktion ist, dh (x) => (y) => z. Da sich der Typparameter Sin der ersten Funktion befindet, wird er sofort nach Aufrufen dieser Funktion instanziiert (ein konkreter Typ abgerufen). Sie können dies in Aktion sehen, indem Sie sich den folgenden Typ ansehen fn:

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

Da im Argument (state) => ({ ...state, b: 2 })keine Informationen darüber verfügbar sind, was werden Ssoll, wird für Typoskript standardmäßig verwendet unknown. Ist Salso jetzt unbekannt, und unabhängig davon, mit was Sie danach anrufen fn, bleibt es unbekannt.

Eine Möglichkeit, dies zu beheben, besteht darin, wie bereits erwähnt, das Typargument explizit anzugeben:

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

Eine andere Möglichkeit besteht darin, Typoskript einige Informationen zu geben, aus denen der Typ abgeleitet werden kann S, z. B. eine Typbeschränkung für den stateParameter:

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

UPDATE Hier ist meine Antwort:

Beispiel:


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)

Wir sollten generische Parameter direkt für die Pfeilfunktion definieren

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

Wir sollten das initialStateArgument und den ReturnType des Reduzierers irgendwie binden

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

Es bedeutet, dass das stateArgument des Reduzierers (Rückruf) immer Teil des Rückgabetyps sein sollte.

Das heißt, wenn Sie versuchen:

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

es wird nicht funktionieren, aber ich denke, es gibt keinen Sinn, es zu tun