0

In Javascript this gets lost when an object's method is passed as a callback parameter like this: run(obj.myfunc). (Reference)

I'd like to get a warning or error in case this is happening in the source code.

  1. How can I trigger a compiler warning ( tsc ?) or linter warning ( eslint ) if the this reference for the object gets lost?
  2. extra: Is there a way to trigger the warning only if loosing this actually has an impact on execution? I.e. if the object in question does not have member variables or does not make use of this, no warning will be issued.
DarkTrick
  • 2,447
  • 1
  • 21
  • 39

1 Answers1

1

Personally, I believe it should be done on eslint level, however there is something you can do to make your code a little bit safer.

Consider this example:

const foo = {
    name: 'foo',
    fn(this) { // this is required if you want to make it type safe
        console.log(this.name)
    }
}



const callback = <Cb extends (this: { name: string }) => void>(cb: Cb) => {
    cb(); // expected error
    cb.call({ name: 'hello', fn: () => { } }) // ok
}

const arrowFn = () => { }

callback(arrowFn)
callback(foo.fn)// error because of contravariance

Playground

As you might have noticed, I have provided fn method with explicit this context notation. It means that context is important for this method (see docs).

Than, function callback expects a callback which is context sensitive. TS forbids you to call cb without context. You are allowed to call cb only with Function.prototype.call, see cb.call({ name: 'hello', fn: () => { } })

However, it is still very tricky. TS allows you to use arrowFn as an argument, because this function does not expect any arguments, just like cb. See this:

declare let withContext: (this: { name: string }) => void
declare let nonContext: () => void

withContext = nonContext // ok
nonContext = withContext // ok

So, why we have an error here ?:

callback(foo.fn) // error

Consider this example:

type ThisA = { name: string }
type ThisB = { name: string, fn: () => void }

// #FIRST EXAMPLE
declare let a: ThisA
declare let b: ThisB
a = b // ok
b = a // error

// #SECOND EXAMPLE
declare let thisA: (this: ThisA) => void
declare let thisB: (this: ThisB) => void

thisA = thisB // error
thisB = thisA // ok

In #first example, b is assignable to a, and it is expected. However, is #second example, thisB is no more assignable to thisA, bit thisA is assignable to thisB. It is called contravariance. See my question about it.

Let's summarize. If you want to pass a callback which is this dependent, you need to explicitly type this dependency:

const foo = {
    name: 'foo',
    fn() {
        console.log(this)
    }
}

const callback = <
    Cb extends (this: { name: string, fn: () => void }) => void
>(cb: Cb) => {
    cb(); // expected error
    cb.call({ name: 'hello', fn: () => { } }) // ok
}

TypeScript has some interesting typings of this in s, my article or this answer

EXAMPLE WITH CALSSES

interface Base {
    name: string
}

class Bar {
    say(this: Base) {
        return this.name
    }
}

class Foo {
    constructor(public name: string) { }

    log(this: Base) {
        console.log(this.name)
    }
}



const callback = <Cb extends (this: Base) => void>(cb: Cb) => {
    cb(); // expected error
    cb.call({ name: 'hello' }) // ok
}

const foo = new Foo('Ternopil')
const bar = new Bar()
bar.say() // not safe, because `bar` don't have own `name` proeprty

// ok, because we expect a callback which requires only `name` property, see Base interface
callback(foo.log)
callback(bar.say) // ok

Playground

Assume we don't care what class it is. What we care is that class method should expect this to have a name property. I have created a Base interface with name property and explicitly defined this type on say method and log method

Please be aware that it is not safe, because bar instance does not have own name property