TypeScript callback function type verification fails-pot gives type inference

< H2 > Preface < / H2 >

I am a beginner of TypeScript, and I have doubts in the section < this parameters in callback function > in the "functions" chapter of the official manual guide of TypeScript.

there is nothing wrong with the logic and running results of the instance provided by the official documentation, but I did a few additional tests myself, which caused some unexpected problems.

Development environment:
VSCode 1.26.1 TypeScript 3.0.1

< H2 > official example < / H2 >

this example is used to provide this correct type judgment

in the callback function.

define a callback interface:

interface UIElement {
    addClickListener(onclick: (this: void, e: Event) => void): void;
}

defines a callback class that provides a method as a callback function:

class Handler {
    info: string;
    onClickGood(this: void, e: Event) {
        // can"t use this here because it"s of type void!
        console.log("clicked!");
    }
}
let h = new Handler();
uiElement.addClickListener(h.onClickGood);// uiElement,

because I lack a class that implements the interface, I define a class myself and implement the UIElement interface, which is also the cause of the problem.

my complete example:

interface UIElement {
    addClickListener(onclick: (this: void, e: string) => void): void;
}

class SideBar implements UIElement {
    addClickListener(onclick: (this: void, e: string) => void): void {
        onclick("event");
    }
}

class Handler {
    info: string = "ok";
    onClickGood(this:void,event:string):void{
        console.log("clicked");
        console.log(event);
    }
}
let h: Handler = new Handler();
let uiElement: SideBar = new SideBar();

uiElement.addClickListener(h.onClickGood)

this example can be run without errors. I have provided a SideBar class. The only change is that the type of Event in the callback function has been changed to String .

example link:

the location of the https://www.tslang.cn/docs/ha.
example at the bottom of the page
< H2 > question < / H2 >
  1. A class implements the method defined by the interface to obtain the callback function, and the method implemented by the class may not verify its internal parameter structure (specific type of callback)

    interface UIElement {
        addClickListener(onclick: (this: void, e: string) => void): void;
    }
    
    class SideBar implements UIElement {
        addClickListener(onclick){
            onclick("event");
        }
    }

    there is no problem with writing this way. Even if the addClickListener method in SideBar does not provide complete type verification, TypeScript will automatically infer it. The evidence is as follows

    class SideBar implements UIElement {
        addClickListener(onclick,hello){
            onclick("event");
        }
    }

    I added an extra parameter to this method. Since there is no definition in UIElement , an error is prompted here, which proves that TypeScript"s type inference works properly, which is in line with our expectations, but then it doesn"t work properly

    .
    interface UIElement {
        addClickListener(onclick: (this: void, e: string) => void): void;
    }
    
    class SideBar implements UIElement {
        addClickListener(onclick){
            onclick("event");
        }
    }
    class Handler {
        info: string;
        onClickGood(this:Handler,event:string):void{
            console.log("clicked!");
            console.log(event);
            
        }
    }
    let h:Handler = new Handler();
    let uiElement:SideBar = new SideBar();
    
    uiElement.addClickListener(h.onClickGood)// 

    in the last sentence of this example, we pass the onClickGood method of the Handler class to the addClickListener
    of SideBar . Note that if the previous structure matches the normal operation effect, the acceptance type of the callback function here should be onclick: (this: void, e: string) = > void
    , but the type we passed in the past is (this:Handler,event:string) = > void is obviously not correct. But there was no error.

    A type mismatch is prompted only if the SideBar is forced to add the same type rules as the UIElement interface.

     class SideBar implements UIElement {
      addClickListener(onclick: (this: void, e: string) => void):void{
          onclick("event");
      }
     }
     class Handler {
         info: string;
         onClickGood(this:Handler,event:string):void{
             // can"t use this here because it"s of type void!
             console.log("clicked!");
             console.log(event);
         }
     }
     let h:Handler = new Handler();
     let uiElement:SideBar = new SideBar();
     
     uiElement.addClickListener(h.onClickGood)// 
  2. the return value of the callback function defined by the interface can actually be inconsistent with the type defined by the interface
    continue to use the previous example:

     interface UIElement {
         addClickListener(onclick: (this: void, e: string) => void): void;
     }
     
     class SideBar implements UIElement {
         addClickListener(onclick: (this: void, e: string) => void) :void{
             onclick("event");
         }
     }
     class Handler {
         info: string;
         onClickGood(this:void,event:string){
             console.log("clicked!");
             console.log(event);
             return 123;
         }
     }
     let h:Handler = new Handler();
     let uiElement:SideBar = new SideBar();
     
     uiElement.addClickListener(h.onClickGood)// 

    notice the onClickGood in Handler here. He returns the number type, and the editor prompt is also of number type, but can pass the test in the last sentence.
    even if we addClickListener requires that the return value of the function is void .

    unless you add a return type to onClickGood as well:

    class Handler {
         info: string;
         onClickGood(this:void,event:string):void{
             // can"t use this here because it"s of type void!
             console.log("clicked!");
             console.log(event);
             return 123;
         }
     }

    now the value of return is finally judged to be incorrect, but isn"t the callback function type rule made in addClickListener ignored

    ?

has been left unanswered for several days, and it may be too difficult to understand. After checking the documents and messing around a few times, some ideas have emerged

problem study

Let's use the complete example I provided earlier:

interface UIElement {
    addClickListener(onclick: (this: void, e: string) => void): void;
}

class SideBar implements UIElement {
    addClickListener(onclick: (this: void, e: string) => void): void {
        onclick('event');
    }
}

class Handler {
    info: string = 'ok';
    onClickGood(this:void,event:string):void{
        console.log('clicked');
        console.log(event);
    }
}
let h: Handler = new Handler();
let uiElement: SideBar = new SideBar();

uiElement.addClickListener(h.onClickGood)

if not strictly speaking, this is actually an observation mode, I provide a JS version of the same function here (no simulation inheritance):

function SideBar(){

}
SideBar.prototype.addClickListener = function (callback){
    callback('event');
}

function Handler(){
    this.info = 'ok';
}
Handler.prototype.onClickGood = function (event){
    console.log(event);
}

let handler = new Handler();
let sidebar = new SideBar();

sidebar.addClickListener(handler.onClickGood);

run output:

event

at this time, if we print onClickGood inside this , the output must be a global object (strict mode is not turned on)

but generally, when using the observer mode, we usually pass in only a function, rather than a method of an object, as follows:

window.addEventListener('click',function (event){
    console.log(event)
})

this is the callback function. The callback function has no state this points to the global.

the original problem is how to get the correct this type in TypeScript, and the solution is to explicitly specify this as void .

interface UIElement {
    addClickListener(onclick: (this: void, e: string) => void): void;
}
class Handler {
    info: string = 'ok';
    onClickGood (this:void,event:string) :void {
        console.log(event);
    }
}

the onClickGood method above and the addClickListener defined by the UIElement interface require that the passed function format be the same, and then your this finally gets the correct type direction.

but there is type inference in TypeScript, which is mentioned in the interface chapter of the official manual guide. We only provide UIElement but not the implementation class. Now we provide an implementation class:

class SideBar implements UIElement {
    addClickListener(onclick: (this: void, e: string) => void): void {
        onclick('event');
    }
}

but if there is correct type inference, we can omit the type constraints in the SideBar , addClickListener methods, but the result is that the method loses its type checking function:

interface UIElement {
    addClickListener(onclick: (this: void, e: string) => void): void;
}
class SideBar implements UIElement {
    addClickListener(onclick) {
        onclick('event');
    }
}
class Handler {
    info: string = 'ok';
    onClickGood (this:Handler,event:string) :void {
        console.log(event);
    }
}

this can be run, but onClickGood does not conform to addClickListener in UIElement accepts the type defined by the function.

is actually not just a function, but it also works a little abnormally for a common type:

interface UIElement {
    addClickListener(onclick:string): void;
}

class SideBar implements UIElement {
    addClickListener(onclick) {
        onclick('event');
    }
}

class Handler {
    info: string = 'ok';
    onClickGood (this:Handler,event:string) :void {
        console.log(event);
    }
}

in this example, the addClickListener acceptance type in UIElement is directly changed to string . This example can still pass the check, of course, it can not be run.

Why

after a lot of trouble, I found a reasonable explanation for all this.

first:

  1. Type inference can only limit one layer

    interface UIElement {
        addClickListener(onclick:string): void;
    }
    
    class SideBar implements UIElement {
        addClickListener(onclick:number) {
            
        }
    }

    in this example, the work type of the implementation class is number , but the interface is specified as string .

    but if we are providing a class that inherits SideBar :

    interface UIElement {
        addClickListener(onclick:string): void;
    }
    
    class SideBar implements UIElement {
        addClickListener(onclick) {
            
        }
    }
    
    class Menu extends SideBar {
        addClickListener(onclick:number){
    
        }
    }

    Menu there is no problem even if it is changed to number type.

  2. The actual type of the parameter under
  3. type inference is any
    actually the type inference constraint is correct and does not allow you to modify the type, but if you do not specify the type, the default is any it enters a subclass

    of any type, and because the previous type is any , subsequent types can specify any type

    .

result

Type inference works well on function interfaces, but not very well on general interfaces.

Type inference in the case of perfect examples:

interface SearchFunc {
    (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function (src, sub) {
    let result = src.search(sub);
    return result > -1;
}

so type inference doesn't work very well for multiple inheritance, we can only write all types, but for callback functions we can define a function interface to simplify the amount of code:

interface EventCallback {
    (this: void, e: string):void;
}

interface UIElement {
    addClickListener(onclick:EventCallback): void;
}

class SideBar implements UIElement {
    addClickListener(onclick:EventCallback): void {
        onclick('event');
    }
}

class Handler {
    info: string = 'ok';
    onClickGood(this: void, event: string): void {
        console.log('clicked');
        console.log(event);
    }
}
let h: Handler = new Handler();
let uiElement: SideBar = new SideBar();

uiElement.addClickListener(h.onClickGood)

< H1 > -- strict < / H1 >

seeing how the subject let h: Handler = new Handler () is written, I estimate that the subject comes from the Java background. JavaScript is all automatically inferred, and cPP11 also has the auto keyword. Only Java put it off until March this year to give it to var . The type on the right is certain, and there is one more type declaration on the left, which is troublesome enough to be written in Java.

Typescript wants to strike a balance between rigor and convenience, rather than obsessing about mathematical correctness, so it allows some types of incompatibility (which Microsoft believes are commonly used). If you want to be as strict as Java as possible, you can turn on the compiler's -- strict option.

The method implemented by the < H1 > class may not verify its internal parameter structure < / H1 >
addClickListener(onclick: (this: void, e: string) => void): void

words: I do not need callback function to return value, even if there is, I guarantee not , so you can return whatever you like.

Menu