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!
Why does using an 'if-else' statement produce a TypeScript compiler error when a seemingly identical ternary operator construct does not?
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.
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.
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;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With