Pourquoi Typescript ne peut-il pas déduire le type d'une fonction générique imbriquée?
Typescript tape l' state
argument de l' applyReducer
appel ci-dessous comme unknown
. Cela fonctionne si je spécifie explicitement le type avec applyReducer<State>
, mais pourquoi est-ce nécessaire? Il semble assez clair que le type devrait être 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)
Terrain de jeu TS
Réponses
Pour répondre à ta question:
Cela fonctionne si je spécifie explicitement le type avec applyReducer, mais pourquoi est-ce nécessaire?
Le problème est avec le fait qu'il s'agit d'une fonction curry, ie (x) => (y) => z
. Parce que le paramètre de type S
est sur la première fonction, il «instanciera» (obtiendra un type concret) dès que vous appelez cette fonction. Vous pouvez le voir en action en regardant le type fn
ci-dessous:
const fn = applyReducer((state) => ({ ...state, b: 2 }))
// ^^^^^^^^ error (because you can't spread 'unknown')
//
// const fn: (state: unknown) => unknown
Parce que dans l'argument, (state) => ({ ...state, b: 2 })
il n'y a pas d'informations disponibles sur ce qui S
devrait devenir, le type par défaut est unknown. Ainsi S
est inconnu maintenant, et peu importe ce que vous appelez fn
par la suite, il restera inconnu.
Une façon de résoudre ce problème consiste - comme vous l'avez mentionné - à fournir explicitement l'argument type:
const fna = applyReducer<State>((state) => ({ ...state, b: 2 }))
Et une autre façon consiste à donner au typographie des informations à partir desquelles il peut déduire le type S
, par exemple une contrainte de type sur le state
paramètre:
const fnb = applyReducer((state: State) => ({ ...state, b: 2 }))
MISE À JOUR Voici ma réponse:
Exemple:
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)
Nous devrions définir le paramètre générique directement pour la fonction de flèche
type UnaryReducer = <S>(state: S) => S
Nous devrions en quelque sorte lier l' initialState
argument et le ReturnType du réducteur
interface ApplyReducer {
<T extends UnaryReducer>(reducer: T): <S,>(state: ReturnType<T> & S) => ReturnType<T> & S;
}
Cela signifie que l' state
argument du réducteur (rappel) doit toujours faire partie du type de retour.
Cela signifie que si vous essayez de:
state => ({ a:0, b: 2, })
ça ne marchera pas, mais je pense qu'il n'y a pas de sens pour le faire