2

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
  • identityOrCall(() => 'x') will not return an error (why would it?) in this case T will be () => string which is a valid type Commented Nov 10, 2020 at 1:37
  • @apokryfos The misunderstanding I have is that I would expect identityOrCall to return the same type as it's argument. If given a Function type as an argument I would expect it to return a Function type. Commented Nov 10, 2020 at 1:47

2 Answers 2

2

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?
Sign up to request clarification or add additional context in comments.

2 Comments

I'm not sure if this is the corrrect explanation. I'm using "noImplicitAny" in my tsconfig.json. This also appears to give no compilation errors: identityOrCall(function (): string { return 'x' })
Actually, I think I understand now. So the T assumes the "loosest" type here which is any, as opposed to immediately assuming the type of the first T typed argument.
1

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.