Search code examples
typescripttypescript-typesconditional-types

How to correctly type a mapOptional function in TypeScript?


I have the following function which takes an optional value and maps it to a different value unless it is null or undefined. My current solution to this looks as follows:

type Return<I, R> = I extends null ? null
                  : I extends undefined ? undefined
                  : R;

/**
 * Maps an optional value to another optional by a callback.
 *
 * @param input - The input value to map.
 * @param callback - The callback to map the value.
 * @returns An optional mapped value.
 */
export function mapOptional<I, R>(input: I, callback: (value: NonNullable<I>) => R): Return<I, R> {
  if (input === null) {
    return null as Return<I, R>;
  }

  if (typeof input === 'undefined') {
    return undefined as Return<I, R>;
  }

  return callback(input!) as Return<I, R>;
}

There are two things which bug me:

  • Why do I have to cast the return values to Return<I, R>?
  • Why do I have to use the bang operator ! so input becomes NonNullable?

An improvement to my solution would be much appreciated!


Solution

  • Right now conditional types have a strange relation with the return types of the function, but in your case you can do a little trick with having an overload that has the widest type possible and also handle edge cases around known null and undefined being passed to the function:

    function mapOptional<I = any, R = unknown>(input: null, callback: (value: I) => R): null
    function mapOptional<I = any, R = unknown>(input: undefined, callback: (value: I) => R): undefined
    function mapOptional<I, R>(input: I, callback: (value: NonNullable<I>) => R)
        : I extends (undefined | null) ? I : R
    function mapOptional<I, R>(input: I, callback: (value: I) => R) {
        if (input === null) {
            return null;
        }
    
        if (input === undefined) {
            return undefined;
        }
    
    
        return callback(input);
    }
    
    mapOptional(undefined, x => x.toFixed()) // type is undefined
    mapOptional(null, x => x + 5) // type is null
    mapOptional(56, x => x + 5).toFixed() // works
    
    declare const optionalNumber: number | undefined
    
    const r = mapOptional(optionalNumber, x => x + 5) // undefined | number
    if (r) {
        console.log(r.toFixed())
    }