Skip to content

Instantly share code, notes, and snippets.

@davidmh
Last active September 27, 2022 16:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save davidmh/0babb329c257f409e987786f897112c9 to your computer and use it in GitHub Desktop.
Save davidmh/0babb329c257f409e987786f897112c9 to your computer and use it in GitHub Desktop.
Why do we need an explicit return on reducer functions?
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