Anthony Naddeo
Anthony Naddeo

Reputation: 2751

Passing callbacks in Typescript - why can the callback's argument not match and compile?

I have a contrived example that was derived from a React callback that I was making.

interface A {
    a: string
    b: string
}

interface B {
    a: string
    b: string
    c: string
}

function foo(fn: (a: A) => void, a: A) {
    fn(a)
}

function bar(arg: B): void {
    console.log(arg.c)
}

foo(bar, {a: 'a', b: 'b'}) // This works, unfortunately

Here, bar is a valid argument to foo, even though bar's first argument has more fields than the one that foo says it should have. That means that someone who supplies bar as a callback here would think that they're going to have a c string in their argument, but they won't.

It's not like there is no type checking on the arguments. The following correctly fails:

function baz(arg: string) { }

foo(baz, {a: 'a', b: 'b'}) // This fails correctly

What exactly is going on here and is there a way of specifying the types such that the behavior is closer to what would be ideal?

Upvotes: 4

Views: 114

Answers (3)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 250006

This seems like a good use case for generics. You want to make sure that the type passed in as the argument is compatible with the function parameter. If these are the same generic parameter, the compiler should check that they are consistent.

interface A {
    a: string
    b: string
}

interface B {
    a: string
    b: string
    c: string
}

function foo<T extends A>(fn: (a: T) => void, a: T & {}) {
    fn(a)
}

function bar(arg: B): void {
    console.log(arg.c)
}

foo(bar, {a: 'a', b: 'b'}) //error

Notice that the a parameter of foo is typed as T&{} not just T. This is done to decrease the priory of that inference site. Since T can be infered from either fn or a we want to make sure the compiler favors fn even if it could pick a type for T that would make both parameters compatible (in this case that type would be A). I don't remember where I found this behavior documented but I'm sure a member of the compiler team said it can be relied upon for the foreseeable future :-)

Upvotes: 3

a better oliver
a better oliver

Reputation: 26838

An interface is a contract that requires a structure to provide certain attributes (or behavior). That is true for any language that supports the concept of interfaces.

Look at this example:

class C implements A, B {
  a: string
  b: string
  c: string    
}

The type C conforms to both A and B. Consequently you should be able to use instances of C wherever either A or B is required. It would be weird if you couldn't.

If you apply for a job that requires a degree in computer science then you are qualified even if you have additional degrees. You would find it disturbing if they rejected you for no other reason than having additional skills.

Upvotes: 0

Amit
Amit

Reputation: 4353

I found this very interesting, I think this would explain why it works, unfortunately not how to disallow a similar situation... :(

https://www.typescriptlang.org/docs/handbook/type-compatibility.html

Upvotes: 0

Related Questions