Today I was writing tests for a React app and I was building a function which allowed me to easily generate the an initial state object to use for rendering components, replacing default values with the ones needed for each particular test case.
The app is a music theory app which visualizes scales and chords on a variety of stringed instruments like guitar, mandolin, etc.
I have the following types for the app's state:
type RootState = {
app: AppState;
instrument: StringedInstrumentState;
}
type AppState = {
showSettingsMenu: boolean;
isPlayingAudio: boolean;
}
type StringedInstrumentState = {
instrumentType: StringedInstrumentName;
tuningName: TuningName;
strings: Tuning;
currentKey: keyof MusicKeys;
scale: {
name: ScaleName;
intervals: Interval[];
notes: NoteName[];
};
}
For the sake of brevity, I'm not including the other types that are used within each of the aforementioned types, but they aren't necessarily important for this post.
The important ones are NoteName
which are just a note name and an octave number ("c4"
, "f#5"
, "gb2"
, etc) and StringedInstrumentName
, which is just a string literal union type consisting of "guitar" | "mandolin"
, since those are the values I was updating in the state object.
For my utility function, I am using lodash's _.mergeWith
method to combine an object of the state values to be updated with the object of default values from the app's store.
import {initialState, RootState} from 'store';
export const buildInitialState = (
stateUpdates: Partial<RootState>
): RootState => {
return _.mergeWith(initialState, stateUpdates, (objVal, srcVal) => {
if (_.isArray(objVal)) {
return srcVal;
}
});
};
The function will return the initial state for the app with updated values for the specified items passed to the function.
I wanted to call this function like this to change the UI to display a mandolin neck instead of the default guitar neck:
const initialState = buildInitialState({
instrument: {
instrumentType: "mandolin",
strings: ["g3", "d4", "a4", "e5"]
}
})
However, that led to this error:
I found that the culprit was my typing of the function's parameter, Partial<RootState>
because RootState.instrument
required the other attributes and only instrumentType
and strings
were being provided in the function's argument.
Instead of making all the attributes optional in the state object's type, I wound up creating a recursive implementation of the Partial utility which makes all nested objects' attributes optional.
type DeepPartial<T> = T extends object ? {
[P in keyof T]? : DeepPartial<T[P]>
} : T;
Breakdown:
T extends object
- if the type given to the generic is an object with keys
[P in keyof T]?
- loop through the keys of the object T
, making each optional with the ?
modifier
DeepPartial<T[P]>
- recursively check the values at each key in the object T
and make optional each of their values
: T
- if the type given to the generic is not an object, use it as it is
When this type was used for the argument of my function, all errors went away and I was able to pass partials of partials of partials all the way down to change whichever bits of state I needed.
When I went to import this type into my files to be used, the intellisense in VS Code informed me that this type already existed as part of @reduxjs/toolkit
, so I used that instead and called it a day. It was a good learning experience to have written it from scratch though.
Top comments (0)