Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Intersect interfaces instead of union #33070

Closed
Rukomoynikov opened this issue Aug 24, 2019 · 4 comments
Closed

Intersect interfaces instead of union #33070

Rukomoynikov opened this issue Aug 24, 2019 · 4 comments

Comments

@Rukomoynikov
Copy link

Rukomoynikov commented Aug 24, 2019

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

@MartinJohns
Copy link
Contributor

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.

@jcalz
Copy link
Contributor

jcalz commented Aug 24, 2019

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.

@jack-williams
Copy link
Collaborator

jack-williams commented Aug 24, 2019

@MartinJohns’s comment shows why the current behaviour is the correct one.

More details can be found here: #29011

@Rukomoynikov
Copy link
Author

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,

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants