8

I have a function that is intended to either return a value of IDBValidKey or something converted to IDBValidKey. If I write the function using the ternary operator it works fine but it causes a compiler error if I write it as an if-else statement:

interface IDBValidKeyConvertible<TConverted extends IDBValidKey> {
    convertToIDBValidKey: () => TConverted;
}

function isIDBValidKeyConvertible<TConvertedDBValidKey extends IDBValidKey>(object: unknown): object is IDBValidKeyConvertible<TConvertedDBValidKey> {
    return typeof((object as IDBValidKeyConvertible<TConvertedDBValidKey>).convertToIDBValidKey) === "function";
}

type IDBValidKeyOrConverted<TKey> = TKey extends IDBValidKeyConvertible<infer TConvertedKey> ? TConvertedKey : TKey;

function getKeyOrConvertedKey<TKey extends IDBValidKey | IDBValidKeyConvertible<any>>(input: TKey): IDBValidKeyOrConverted<TKey> {
    if (isIDBValidKeyConvertible<IDBValidKeyOrConverted<TKey>>(input)) {
        return input.convertToIDBValidKey();
    } else {
        return input;
    }
}

function getKeyOrConvertedKeyTernary<TKey extends IDBValidKey | IDBValidKeyConvertible<any>>(input: TKey): IDBValidKeyOrConverted<TKey> {
    return (isIDBValidKeyConvertible<IDBValidKeyOrConverted<TKey>>(input)) ? input.convertToIDBValidKey() : input;
}

getKeyOrConvertedKeyTernary produces no errors but the else block of getKeyOrConvertedKey yields this error:

Type 'TKey' is not assignable to type 'IDBValidKeyOrConverted<TKey>'.
  Type 'string | number | Date | ArrayBufferView | ArrayBuffer | IDBArrayKey | IDBValidKeyConvertible<any>' is not assignable to type 'IDBValidKeyOrConverted<TKey>'.
    Type 'string' is not assignable to type 'IDBValidKeyOrConverted<TKey>'.

Aren't the ternary operator and the if-else statement equivalent?

Thanks!

2
  • 1
    Looking through the AST it seems that the difference comes down to ReturnStatement > ConditionalExpression > Binary Expression > ... versus IfStatement > ((BinaryExpression > ...) + (Block ReturnStatement) + (Block ReturnStatement). Having said that, I'm not sure why that would cause this result... Great question! Commented May 8, 2020 at 19:40
  • 1
    My gut feeling is that the scope in which the type guard is applied isn't really compatible with ternaries. Most likely a good candidate for an issue on Github unless Ryan Cavanaugh or Anders Hejlsberg read this question before posting an issue. Commented May 8, 2020 at 23:38

2 Answers 2

2

Why does using an 'if-else' statement produce a TypeScript compiler error when a seemingly identical ternary operator construct does not?

Short Answer

TypeScript sees an if-else as a statement with multiple expressions that each have independent types. TypeScript sees a ternary as an expression with a union type of its true and false sides. Sometimes that union type becomes wide enough for the compiler not to complain.

Detailed Answer

Aren't the ternary operator and the if-else statement equivalent?

Not quite.

The difference stems from a ternary being an expression. There is a conversation here where Ryan Cavanaugh explains the difference between a ternary and an if/else statement. The take home is that the type of a ternary expression is a union of its true and false results.

For your particular situation, the type of your ternary expression is any. That is why the compiler does not complain. Your ternary is a union of the input type and the input.convert() return type. At compile time, the input type extends Container<any>; therefore the input.convert() return type is any. Since a union with any is any, the type of your ternary is, well, any.

A quick solution for you is to change any to unknown in <TKey extends IDBValidKey | IDBValidKeyConvertible<any>. That will make both the if-else and the ternary produce a compiler error.

Simplified Example

Here is a playground link with a simplified reproduction of your question. Try changing the any to unknown to see how the compiler responds.

interface Container<TValue> {
  value: TValue;
}

declare function hasValue<TResult>(
  object: unknown
): object is Container<TResult>;

// Change any to unknown.
const funcIfElse = <T extends Container<any>>(input: T): string => {
  if (hasValue<string>(input)) {
    return input.value;
  }

  return input;
};

// Change any to unknown.
const funcTernary = <T extends Container<any>>(input: T): string =>
  hasValue<string>(input)
    ? input.value
    : input;
Sign up to request clarification or add additional context in comments.

2 Comments

Nice find Shaun! I linked to your answer in mine. I don't quite get Ryan Cavanaugh's explanation as to why Typescript can't do better, but my head is tired :P
Thank you for taking the time to read the question and provide the explanation; it helped me understand the issue!
0

There are two totally separate problems going on:

  1. Typescript has a bug or limitation[1], but it is the opposite of what your question assumes.

    if I write the function using the ternary operator it works fine

    Actually, it's not fine. The error for the if-else version is correct, and the ternary version should have the same error.

    You probably made the assumption that you did because, like most of us, we are inclined to assume our code is correct. Which leads to the second problem:


  1. Your code is illogical. (Please excuse the bluntness.)

I think (with the now simplified code in the question) that #2 should be easier to see. But when I get some free time I'll try to explain in detail.


[1] There is a possibility that this isn't a Typescript flaw, but expected behavior due to some subtle non-equivalence between if-else and ? : that I don't know about. I wouldn't be surprised, because Javascript has a bunch of legacy weirdness. [EDIT: See Shaun Luttin's answer which came in just as I was typing mine.]

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.