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