'Why is the return type of `myOverloadedFunction(foo as any)` different from `ReturnType<typeof myOverloadedFunction>`?

I have a simple overloaded function like this:

function myFunction(input: string): string
function myFunction(input: undefined): undefined
function myFunction(input: string | undefined): string | undefined
function myFunction(input: string | undefined): string | undefined {
    return input
}

This function works as expected with properly typed input:

const result1 = myFunction('foo') // Inferred type of result1: string
const result2 = myFunction(undefined) // Inferred type of result2: undefined
const input3: string | undefined = 'foo'
const result3 = myFunction(input3) // Inferred type of result3: string | undefined

However, when input is typed as any, the compiler assumes that the first overload is being used, even though an any can be undefined:

const result4 = myFunction(undefined as any) // Inferred type of result4: string

This is surprising to me (and incorrect!). I would expect result4 to be inferred as ReturnType<typeof myFunction> (which is string | undefined) instead.

What's going on here? What's the motivation for this behavior?



Solution 1:[1]

I stumbled upon this trick from the react-query codebase, and adapted it into an any-safe solution to this question:

type NonOptional<T> = Exclude<T, undefined | null>

// Needs a better name than SafeMap, I'm open to suggestions
type SafeMap<TIn, TOut> = 0 extends 1 & TIn
  ? TOut | undefined
  : TIn extends NonOptional<TIn>
  ? NonOptional<TOut>
  : TIn extends undefined
  ? undefined
  : TOut | undefined;

function parseDate<T extends string | undefined>(arg: T): SafeMap<T, Date> {
  // Fake implementation to satisfy the compiler for the sake of this example
  return undefined as unknown as SafeMap<T, Date>
}

const anyArg: any = undefined
const stringArg: string = ''
const undefinedArg: undefined = undefined
const stringOrUndefinedArg: string | undefined = ''

const anyResult = parseDate(anyArg) // inferred type: `Date | undefined`
const stringResult = parseDate(stringArg) // inferred type: `Date`
const undefinedResult = parseDate(undefinedArg) // inferred type: `undefined`
const stringOrUndefinedResult = parseDate(stringOrUndefinedArg) // inferred type: `Date | undefined`
const _ = parseDate(42) // compilation error because 42 is not assignable to `string | undefined`

This works because any violates the normal rules of types--you might expect 1 & any to be 1, but it's actually any. So 0 extends 1 & any, but not 1 & AnyTypeOtherThanAny.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Matthew