TypeScript function passes type check when not expected to

Viewed 502

I have a function identityOrCall that either calls the function given to it, or returns the value. value is a generic type.

function identityOrCall <T>(value: T): T {
  if (typeof value === 'function')
    return value()
  else
    return value
}

identityOrCall(() => 'x') // -> 'x'

The line identityOrCall(() => 'x') appears to pass the compiler's type checking.

Why? Shouldn't it give an error?

If identityOrCall is passed a function, I would expect the generic type T to be set to Function and that identityOrCall should return a Function type.

Instead I'm able to pass a Function type as an argument and have identityOrCall return a string type.

Why is this? It appears inconsistent to me, but I must be missing something.

2 Answers

The problem is that, since the function return type is not noted anywhere, TypeScript takes its type to be any. any is not type-safe; it's assignable to anything. Here, TS extracts T from the any return value (since any can be narrowed down to T).

Best to avoid types of any whenever possible. I like using TSLint's no-unsafe-any which forbids this sort of thing.

For similar reasons, the following does not throw a TypeScript error (and is prohibited using the above linting rule), even though it's clearly unsafe:

declare const fn: () => any;
function identityOrCall<T>(value: T): T {
    return fn();
}

const result = identityOrCall(() => 'x') // -> 'x'
// result is typed as: () => 'x'. Huh?

If the goal is to have a method behave differently based on the parameters I think the easiest way is to specify overloads

function identityOrCall<T>(value: () => T): T;
function identityOrCall<T>(value: T): T;

function identityOrCall <T>(value: () => T|T): T {
  if (typeof value === 'function')
    return value()
  else
    return value
}

const result = identityOrCall(() => 'x') // first overload
const result2 = identityOrCall('x') // second overload

result.split(' ');  // No error
result2.split(' '); // No error
result(); // error not callable

When calling the method TS will resolve the signature by going through the overloads until it finds one which matches (meaning order of defining overloads matters here).

Playground link

Related