TypeScript Version: 3.4.0-dev.201xxxxx
Search Terms: Intersection interfaces, Ternary operator, Intersect interfaces instead of union
Code
const someCondition = true
interface Func1Options { name: string }
interface Func2Options { age: number }
function Func1 (options: Func1Options) { console.log(options) }
function Func2 (options: Func2Options) { console.log(options) }
const selectedFunc = someCondition
? Func1
: Func2
selectedFunc({
age: 20,
name: 'Alastar'
})
Expected behavior:
selectedFunc accepts Func1Options
Actual behavior:
selectedFunc accepts Func1Options & Func2Options
Playground Link: Playground
But that would produce invalid results. If selectedFunc would be an intersection, then it would be appear as an overloaded function, even tho it's not.
For example if you manually force an intersection:
const someCondition = false
interface Func1Options { name: string }
interface Func2Options { age: number }
function Func1 (options: Func1Options) { console.log(options.name) }
function Func2 (options: Func2Options) { console.log(options.age) }
type BothFunc = typeof Func1 & typeof Func2
const selectedFunc = (someCondition ? Func1 : Func2) as BothFunc
const obj: Func1Options = { name: 'Bob' }
selectedFunc(obj)
The compiler will assume it calls Func1, because that's what the obj parameter matches to. But it actually calls Func2 which gets an invalid object passed along.
Is this issue saying that selectedFunc should be of type typeof Func1 because the condition in the ternary is statically known to be true? If so, that is working as intended as per #14206. As far as I know, the type of x ? y : z is always (equivalent to) typeof y | typeof z regardless of whether x is known to be truthy or falsy.
Or is this issue saying that selectedFunc should be of type typeof Func1 | typeof Func2, but that you should be able to call it with a Func1Options | Func2Options argument? If so, then this is working as intended, as implemented in #29011, which is a partial fix for #7294. It is not safe for a union of functions to accept a union of arguments, because function arguments are contravariant. Consider the following code:
function Func1(options: Func1Options) {
options.name.toUpperCase();
}
function Func2(options: Func2Options) {
options.age.toFixed();
}
const selectedFunc = Math.random() < 0.99 ? Func1 : Func2;
selectedFunc({ age: 20 }); // 99% guarantee of kaboom
At runtime, selectedFunc may turn out to be Func1. Inside the selectedFunc({age: 20}) call, options.name.toUpperCase() will end up trying to call a method on undefined, and you will get a runtime error. It's not safe. Unless you can narrow selectedFunc to either typeof Func1 or typeof Func2, the only safe thing you can pass it as an argument is Func1Options & Func2Options. That way you know no matter which function it turns out to be, it will have an argument that meets its needs.
@MartinJohns鈥檚 comment shows why the current behaviour is the correct one.
More details can be found here: #29011
Is this issue saying that
selectedFuncshould be of typetypeof Func1because the condition in the ternary is statically known to betrue? If so, that is working as intended as per #14206. As far as I know, the type ofx ? y : zis always (equivalent to)typeof y | typeof zregardless of whetherxis known to be truthy or falsy.Or is this issue saying that
selectedFuncshould be of typetypeof Func1 | typeof Func2, but that you should be able to call it with aFunc1Options | Func2Optionsargument? If so, then this is working as intended, as implemented in #29011, which is a partial fix for #7294. It is not safe for a union of functions to accept a union of arguments, because function arguments are contravariant. Consider the following code:function Func1(options: Func1Options) { options.name.toUpperCase(); } function Func2(options: Func2Options) { options.age.toFixed(); } const selectedFunc = Math.random() < 0.99 ? Func1 : Func2; selectedFunc({ age: 20 }); // 99% guarantee of kaboomAt runtime,
selectedFuncmay turn out to beFunc1. Inside theselectedFunc({age: 20})call,options.name.toUpperCase()will end up trying to call a method onundefined, and you will get a runtime error. It's not safe. Unless you can narrowselectedFuncto eithertypeof Func1ortypeof Func2, the only safe thing you can pass it as an argument isFunc1Options & Func2Options. That way you know no matter which function it turns out to be, it will have an argument that meets its needs.
Thank you, for description. Now I understand kind of philosophy of ts,
Most helpful comment
Is this issue saying that
selectedFuncshould be of typetypeof Func1because the condition in the ternary is statically known to betrue? If so, that is working as intended as per #14206. As far as I know, the type ofx ? y : zis always (equivalent to)typeof y | typeof zregardless of whetherxis known to be truthy or falsy.Or is this issue saying that
selectedFuncshould be of typetypeof Func1 | typeof Func2, but that you should be able to call it with aFunc1Options | Func2Optionsargument? If so, then this is working as intended, as implemented in #29011, which is a partial fix for #7294. It is not safe for a union of functions to accept a union of arguments, because function arguments are contravariant. Consider the following code:At runtime,
selectedFuncmay turn out to beFunc1. Inside theselectedFunc({age: 20})call,options.name.toUpperCase()will end up trying to call a method onundefined, and you will get a runtime error. It's not safe. Unless you can narrowselectedFuncto eithertypeof Func1ortypeof Func2, the only safe thing you can pass it as an argument isFunc1Options & Func2Options. That way you know no matter which function it turns out to be, it will have an argument that meets its needs.