Is it OK to cast undefined to void in TS?

1.4k views Asked by At

TLDR;

Is this OK? Or is it bad practice?

function isUndefined (payload: any): payload is undefined | void {
  return payload === undefined
}

Context

In TypeScript, I have a function that can return either something or undefined or void.

Something like an event handler that can return a modified payload or the dev can choose to return nothing or undefined in case they won't modify the payload:

function eventHandler <T extends {[key: string]: any}> (payload: T): Modified<T> | undefined | void {
  // ... implementation
}

Then I have a type checker that needs to check if it's returning something other than void or undefined:

const result = eventHandler(payload)

if (result !== undefined) {
  // we have a modified payload!
}

However, is the above snippet I get an error because it says that even though result !== undefined it can still be void?

In my opinion, I think it's peculiar because void should be the same as undefined.

So I made this type checker that solves it:

function isUndefined (payload: any): payload is undefined | void {
  return payload === undefined
}

This solves my issue, but my question is:

Is this OK? Or is it bad practice?

2

There are 2 answers

0
cauchy On BEST ANSWER

void is not undefined. void means the absence of a return value. undefined is the type of the value undefined at runtime.

it is true that a function that returns no value at run time returns undefined, but in the TS type system we chose to make the absence of a return value special.

For example assigning (a) => void to (a) => number | undefined is likely an error, though it is safe at run-time.

In general do not use void except in the return type of functions. for everything else, use undefined.

So, yes we will need to use different check for undefined and void.

0
Alex Wayne On

I think you're making this more complicated that it needs to be. A function that returns void can:

  1. Have no return statement
  2. Have a return; statement with no value specified.
  3. Have a return undefined; statement.

In pure javascript, all of the above would have a return value of undefined. If you say a function returns undefined, then you can only do #2 and #3 from the above list.

So you can just have a function type that unions void with whatever something you want.

function foo(): string | void {
    return Math.random() > 0.5 ? 'abc' : 123
}

const val = foo()
if (val === undefined) {
    console.log('is undefined', val)
} else {
    console.log('is some value', val)
}

This means that you could create a generic function type that modifies payloads like so:

type PayloadModifier<T extends {[key: string]: any}> = (payload: T) => T | void

const setMaxAsTen: PayloadModifier<{a: number}> = (payload) => {
    if (payload.a > 10) {
        return { a: 10 }
    }
    return undefined // required unless noImplicitReturns is false
}

const val = setMaxAsTen({a: 5})
if (val === undefined) {
    console.log('is undefined', val)
} else {
    console.log('is some value', val)
}

Playground

Last thing to note is that there is a compiler option that is good to leave on called noImplicitReturns. if a function declares a return value in any execution branch, then a return value must be declared in every execution branch. So because the above sometimes returns a value, you must explicitly return undefined if you don't to return a payload. You can toggle off that option, allowing you to omit that line, but it's not recommended as it does help you catch some bugs.