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

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;