Software Architect / Entrepreneur

Software Architect / Entrepreneur

Typescript react native

TypeScript useReducer and the ‘undefined’ is not assignable error

Solving the frustrating TypeScript error
Share on facebook
Facebook
Share on twitter
Twitter
Share on linkedin
LinkedIn

Typing everything in TypeScript comes with a lot of surprises because oftentimes the error you’re given is cryptic. Sometimes the error doesn’t say “you’re missing X”. It mostly only infers “You are not conforming to these requirements”.  For this reason, you have to actually use your analytic skills to figure out the problem root problem.

TypeScript, useReducer and the woes around it

One thing that does help to debug a TypeScript problem is to understand what you’re expecting and what you’re actually encountering. In my case I was seeing an error of “undefined” coming from a type definition where I never specified anything about undefined. That was the biggest clue as to what was going on.

TypeScript error in React Native showing Type 'undefined' is not assignable to type

This is the code that I had for my reducer:

				
					const songStatusReducer: Reducer<SongStatusState, SongStatusAction> = (state: SongStatusState, action: SongStatusAction) => {
    switch (state.status) {
        case 'INIT':
            if (action.type === "SET_SONGIDS") {
                let songIndicatorMap = new Map()

                action.songIds.forEach((value) => songIndicatorMap.set(value, false))
                return {
                    status: "READY",
                    currentPlayingSong: null,
                    songIndicatorMap
                }
            }

            break;

        case 'READY':
            if (action.type === "TOGGLE") {
                if (state.currentPlayingSong !== action.songId) {
                    for (let key in state.songIndicatorMap) {
                        state.songIndicatorMap.set(key, false)
                    }

                    state.songIndicatorMap.set(action.songId, true)
                    state.currentPlayingSong = action.songId

                    return {...state}
                }

                if (state.currentPlayingSong === action.songId) {
                    let currentSongStatus = state.songIndicatorMap.get(action.songId)
                    state.songIndicatorMap.set(action.songId, !currentSongStatus)

                    return {...state}
                }
            }

            break;
    }
}
				
			

This didn’t make any sense to me since my code looked right. There weren’t any blatant syntax errors or type mismatches. So I dug a little further.

I looked into the definition of the Reducer generic. The implementation for that looks like this:

				
					type Reducer<S, A> = (prevState: S, action: A) => S;
				
			

So this reads as, “The type Reducer takes two generic type parameters, S and A. The type passed in as S will be used for the prevState function argument and the return type of the function. The type passed in for A will be used for the function argument action.”

One thing I realized here for the Reducer type is that a return type is part of the requirement of making that type valid. My reducer didn’t specify a return type. So that’s what I did next. Now my reducer signature looked like the following:

				
					const songStatusReducer: Reducer<SongStatusState, SongStatusAction> = (state: SongStatusState, action: SongStatusAction): SongStatusState => {
...
				
			

Now I get a new TypeScript error on my useReducer function.

				
					Function lacks ending return statement and return type does not include 'undefined'.
				
			

At this point I had an error that ACTUALLY MADE SENSE. I was missing a return statement for a default condition!

I'm not returning "undefined" in my function! What gives!?

This will make sense when you realize that in Javascript a default value will be returned if you don’t add a return statement to a function. The default that is returned is undefined. So basically I wasn’t covering a situation in my reducer where it was possible to return a value of undefined.

The fix was simple and I had two options. Option #1 was to just add a return value passing back the previous state. Option #2 was to halt the execution of the function and therefore not even allowing a return value to happen. That’s possible by throwing an Error object. I chose this path because I consider that if my reducer is given a status that is invalid, that status should be rejected.

Given that, here’s the final reducer. Notice the error I throw and where I throw it:

				
					{
    switch (state.status) {
        case 'INIT':
            if (action.type === "SET_SONGIDS") {
                let songIndicatorMap = new Map()

                action.songIds.forEach((value) => songIndicatorMap.set(value, false))
                return {
                    status: "READY",
                    currentPlayingSong: null,
                    songIndicatorMap
                }
            }

            break;

        case 'READY':
            if (action.type === "TOGGLE") {
                if (state.currentPlayingSong !== action.songId) {
                    for (let key in state.songIndicatorMap) {
                        state.songIndicatorMap.set(key, false)
                    }

                    state.songIndicatorMap.set(action.songId, true)
                    state.currentPlayingSong = action.songId

                    return {...state}
                }

                if (state.currentPlayingSong === action.songId) {
                    let currentSongStatus = state.songIndicatorMap.get(action.songId)
                    state.songIndicatorMap.set(action.songId, !currentSongStatus)

                    return {...state}
                }
            }

            break;
    }

    throw new Error('Invalid status')
}

				
			

Typing State and Actions for useReducer

If you’re wondering how I’m typing the rest of my reducer here are those pieces

				
					type SongStatusState =
    {
        status: 'INIT';
    }
    | {
        status: 'READY';
        currentPlayingSong: string | null;
        songIndicatorMap: Map<string,  boolean>;
    }

type SongStatusAction =
    {
        type: 'SET_SONGIDS';
        songIds: string[];
    }
    | {
        type: 'TOGGLE';
        songId: string;
    }

const songStatusInitial: SongStatusState = {status: 'INIT'}

...

const [songStatusState, songStatusDispatch] = useReducer(songStatusReducer, songStatusInitial)
				
			

I hope that helps you figuring out TypeScript errors, especially ones related to generics. I always recommend looking at the actual implementations of the type definitions to see how the generics are used. This will give you hints as to why TypeScript is complaining.

Email Notifications

Never miss a post again! Get my latest articles delivered directly to your inbox and be the first to know about my upcoming course!

Email Notifications

Never miss a post again! Get my latest articles delivered directly to your inbox and be the first to know about my upcoming course!

🙏

Great Choice!

Thanks for enabling notifications! Don’t worry, I hate spam too and I won’t ever disclose your contact information to 3rd parties.