Typescript: Intersect interfaces instead of union

Created on 24 Aug 2019  路  4Comments  路  Source: microsoft/TypeScript

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

Most helpful comment

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.

All 4 comments

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 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.

Thank you, for description. Now I understand kind of philosophy of ts,

Was this page helpful?
0 / 5 - 0 ratings