Last active
September 27, 2022 16:25
-
-
Save davidmh/0babb329c257f409e987786f897112c9 to your computer and use it in GitHub Desktop.
Why do we need an explicit return on reducer functions?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
interface State { | |
propA: number; | |
propB: string; | |
} | |
/** | |
* TypeScript is a structural type system. This means as long as your data | |
* structure satisfies a contract, TypeScript will allow it. Even if you have | |
* too many keys declared. | |
* | |
* That means that we should be able to find extraneous properties with a | |
* helper like this. | |
* | |
* @link https://fettblog.eu/typescript-match-the-exact-object-shape/ | |
* | |
*/ | |
export type ValidateShape<T, Shape> = T extends Shape | |
? Exclude<keyof T, keyof Shape> extends never | |
? T | |
: never | |
: never; | |
/* The problem comes from returning union types from the reducer. Typescript | |
* seems to ignore the elements that don't match the expected type, and allows | |
* the extra properties to go unnoticed. | |
*/ | |
interface Action<NewState> { | |
reducer: (state: State) => ValidateShape<NewState, State>; | |
} | |
function defineAction<NewState>( | |
reducer: (state: State) => ValidateShape<NewState, State>, | |
): Action<NewState> { | |
return { reducer }; | |
} | |
/** | |
* Successful scenarios | |
*/ | |
// Pass because propA does exist in State | |
export const fnA = defineAction((state) => { | |
return Math.random() > 0.5 ? { ...state, propA: 5 } : state; | |
}); | |
// Fails because extraneousProp doesn't exist in State | |
export const fnB = defineAction((state) => { | |
// ^ Type '{ extraneousProp: number; propA: number; propB: string; }' | |
// is not assignable to type 'never'. | |
return { | |
...state, | |
extraneousProp: 5, | |
}; | |
}); | |
/** | |
* Failed scenarios | |
*/ | |
// This should fail, but it doesn't | |
export const fnC = defineAction((state) => { | |
// The problem with this function not throwing an error seems to relate to | |
// the return type of fnC being the union: State | { extraneousProp: number; propA: number; propB: string } | |
// Hover `result` to see its type | |
const result = Math.random() > 0.5 ? { ...state, extraneousProp: 5 } : state; | |
return result; | |
}); | |
/** | |
* Solution | |
*/ | |
// The expected error only shows up when we add a return type | |
export const fnD = defineAction((state): State => { | |
return Math.random() > 0.5 ? { ...state, extraneousProp: 5 } : state; | |
// ^ Object literal may only specify known properties, | |
// and 'extraneousProp' does not exist in type 'State'. | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment