Warum kann Typescript nicht auf den Typ einer verschachtelten generischen Funktion schließen?
Typescript gibt das state
Argument für den folgenden applyReducer
Aufruf 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
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 S
in 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 S
soll, wird für Typoskript standardmäßig verwendet unknown. Ist S
also 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 state
Parameter:
const fnb = applyReducer((state: State) => ({ ...state, b: 2 }))
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 initialState
Argument 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 state
Argument 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