1
votes

First of all, there is a similar question here: what-is-the-syntax-for-typescript-arrow-functions-with-generics

But, I'd like to know the culprit of the syntax error.

I am using an external library, and this is what the definition file (index.d.ts) looks like:


External Library's index.d.ts

declare namespace Student {
    export interface Lecture {
        lectureName: string;
    }

    export interface Student {
        new (): Student;

        on1(eventName: string, callback: (<T>(lecture: T, oldLecture: T) => void) |
                                        ((name: string, ...args: any[]) => void)): void;

        on2(eventName: string, callback: (<T>(lecture: T, oldLecture: T) => void)): void;
    }
}

declare var Student: Student.Student;

declare module "student" {
    export = Student;
}

Note that there are two functions: on1 and on2 in Student.Student - the function on1 has a bit more code.

So here are my code examples.


Case 1

import * as Student from 'student';
import { Lecture } from 'student';

export class MyStudent { 
    student: Student.Student;

    constructor() {
        this.student = new Student();

        this.student.on1('test', (lecture: Lecture, oldLecture: Lecture) => {
            // Argument of type error
        });

        this.student.on2('test', (lecture: Lecture, oldLecture: Lecture) => {
            // Argument of type error
        });
    }
}

The function on1 gives the below error:

Argument of type '(lecture: Lecture, oldLecture: Lecture) => void' is not assignable to parameter of type '((lecture: T, oldLecture: T) => void) | ((name: string, ...args: any[]) => void)'. Type '(lecture: Lecture, oldLecture: Lecture) => void' is not assignable to type '(name: string, ...args: any[]) => void'. Types of parameters 'lecture' and 'name' are incompatible. Type 'string' is not assignable to type 'Lecture'.

The function on2 gives the below error:

Argument of type '(lecture: Lecture, oldLecture: Lecture) => void' is not assignable to parameter of type '(lecture: T, oldLecture: T) => void'. Types of parameters 'lecture' and 'lecture' are incompatible. Type 'T' is not assignable to type 'Lecture'.

I thought this example is the right way to implement the code - but why this gives an error?


Case 2

import * as Student from 'student';
import { Lecture } from 'student';

export class MyStudent { 
    student: Student.Student;

    constructor() {
        this.student = new Student();

        this.student.on1('test', <Lecture>(lecture: Lecture, oldLecture: Lecture) => {
            lecture.lectureName; 
            // Error: Property 'lectureName' does not exist on type 'Lecture'
        });

        this.student.on2('test', <Lecture>(lecture: Lecture, oldLecture: Lecture) => {
            lecture.lectureName;
            // Error: Property 'lectureName' does not exist on type 'Lecture'
        });
    }
}

In this example, I put <Lecture> in front of the arrow function - so there is no error in the implementation, but now I cannot use lecture.lectureName at all. Why?


Case 3

import * as Student from 'student';
import { Lecture } from 'student';

export class MyStudent { 
    student: Student.Student;

    constructor() {
        this.student = new Student();

        this.student.on1('test', <T extends Lecture>(lecture: T, oldLecture: T) => {
            lecture.lectureName; // Yay! No problem!
        });

        this.student.on2('test', <T extends Lecture>(lecture: T, oldLecture: T) => {
            // Argument of type error
        });
    }
}

So this example has the correct answer - however, the function on2 still gives the argument of type error, just like the case 1's example. Shouldn't it be okay since the function on1 is okay?


Case 4

import * as Student from 'student';
import { Lecture } from 'student';

export class MyStudent { 
    student: Student.Student;

    constructor() {
        this.student = new Student();

        this.student.on1('test', () => () => (lecture: Lecture, oldLecture: Lecture) => {
            lecture.lectureName; // Yay! No error!
        });

        this.student.on2('test', () => () => (lecture: Lecture, oldLecture: Lecture) => {
            lecture.lectureName; // Yay! No error!
        });
    }
}

I found this solution accidentally - and both functions are working fine. But I have no idea why this is working.


I spent some time trying to figure out the exact cause by looking at these references (because I love TypeScript):

but I am still wondering the exact cause of this issue.

3

3 Answers

1
votes

I know this doesn't answer your question directly but you can avoid this issue by using class methods, instead of nested anonymous callbacks (which feels very 2015)

type Handler = <T>(t: T) => void;

class Student {
  on1(s:string, callback:Handler) : void {
    callback<string>("hi")
  }
}

class MyStudent { 
    student: Student

    constructor() {
        this.student = new Student()
        this.student.on1('test', this.log)
    }

    log<T>(t:T) : void { 
         console.log("hi " + t)
    }
}
1
votes

You are a bit confused by how to declare a generic function and how to call a generic function.

You can summarize your issue with this:

// Define the Identity function type
// The result type = input type
type TIdentityFunc = <T>(input: T) => T;

// Implement the TIdentity function
// We followed the rule.
const identityImpl: TIdentityFunc = <T>(input: T) => input;

// Now we call this implementation
const num = identity(5);   // num is always number
const str = identity('hi') // str is always a string

In your example you implemented the requested callback, this means that when someone will call this callback, she will know the parameters types.

Remember, you are not calling the callback, you are only implementing it!

So your code should look like this:

import * as Student from 'student';
import { Lecture } from 'student';

export class MyStudent {
  student: Student.Student;

  constructor() {
    this.student = new Student();

    this.student.on1('test', <T>(l1: T | string, l2: T, ...args) => {
      // This is a bit complicated overloading, 
      // But it follows the rules of the declaration
    });

    this.student.on2('test', <T>(lecture: T, oldLecture: T) => {
      // Your only assumption is that lecture, and oldLecture are the same type
    });
  }
}
1
votes

The problem is the typing was declaring the generic at the wrong place:

declare interface Lecture {
  lectureName: string;
}

declare interface Student {
  new (): Student;

  on1<T>(eventName: string, callback: ((lecture: T, oldLecture: T) => void) |
    ((name: string, ...args: any[]) => void)): void;
  on2<T>(eventName: string, callback: (lecture: T, oldLecture: T) => void): void;
}

let s: Student;

s.on1('x', (a: Lecture, b: Lecture) => {

})
s.on2('y', (a: string, b: string) => {

})