Pourquoi Typescript ne peut-il pas déduire le type d'une fonction générique imbriquée?

Nov 24 2020

Typescript tape l' stateargument de l' applyReducerappel 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

JeffreyWesterkamp Nov 24 2020 at 03:56

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 Sest 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 fnci-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 Sdevrait devenir, le type par défaut est unknown. Ainsi Sest inconnu maintenant, et peu importe ce que vous appelez fnpar 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 stateparamètre:

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

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' initialStateargument 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' stateargument 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