Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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!

like image 230
Auth Infant Avatar asked May 08 '20 18:05

Auth Infant


1 Answers

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;
like image 175
Shaun Luttin Avatar answered Nov 11 '22 01:11

Shaun Luttin