How to create an object from an interface that contains both data and delegate (TypeScript)?

I understood the following,

interface Teacher {
  id: number;
  name: string;
}

interface Func {
  (x: number, y: number): number;
}

const teacher: Teacher = {
  id: 1,
  name: "Aaron",
};
const add: Func = (x, y) => x + y;

but I have no idea how to create an object from the following interface?

interface TeacherFunc {
  id: number;
  name: string;
  (x: number, y: number): number;
}



// It is incomplete.
const tf: TeacherFunc = {
  id: 1,
  name: "Aaron",
};

Answer

Every function in JavaScript is an object, (specifically a Function object), but you cannot use object literal notation to create a function object. If you want to make a function object with additional properties on it, you will need to make a function and then add properties to it, either individually by setting them, or all at once by something like Object.assign().

First let’s change TeacherFunc‘s definition slightly:

interface TeacherFunc {
  id: number;
  teacherName: string;  // <-- changed name to teacherName
  (x: number, y: number): number;
}

I’ve decided to use the property named teacherName instead of name, for reasons I will explain later.

Anyway, you can make a TeacherFunc with Object.assign(), which copies properties to its first argument from its subsequent arguments:

const tf: TeacherFunc = Object.assign(
  (x: number, y: number) => x + y,
  {
    teacherName: "Aaron",
    id: 1,
  }
);

Or you can just make it a function first and add properties individually later, and the compiler will accept it due to support added in TypeScript 3.1 for property declarations on functions:

const tf2: TeacherFunc = (x, y) => x + y;
tf2.teacherName = "Aaron";
tf2.id = 1;

So that’s how you’d do it if you had teacherName instead of name.


But you had this:

interface TeacherFunc {
  id: number;
  name: string; // <-- back to name
  (x: number, y: number): number;
}

The version with name is tricky and weird, because all functions have a read-only name property. So you’ll get a name property for free just by making it a function, but you won’t be able to change it by assignment. If you want that name to be "Aaron", you could try making the function itself named Aaron, which will work that way at least without minifiers or other JS transformation. Or you could use Object.defineProperty() to set it.

So it could look like this:

const tf: TeacherFunc = Object.assign(
  function Aaron(x: number, y: number) { return x + y },
  {
    id: 1,
  }
);
console.log(tf.name); // "Aaron" hopefully

or this:

const Aaron: TeacherFunc = (x, y) => x + y;
Aaron.id = 1;
const tf2 = Aaron;
console.log(tf2.name); // "Aaron" hopefully

or this:

const tf3: TeacherFunc = (x, y) => x + y;
tf3.id = 1;
Object.defineProperty(tf3, "name", { value: "Aaron" });
console.log(tf3.name); // "Aaron" hopefully

But really, I’d stay away from any interface that was both callable and had a non-readonly name property, because it will let you do things that actual JavaScript functions will reject:

tf3.name = "oops" // no compiler error, but
// 💥 ERROR at runtime: "name" is read-only

So I’d stick with teacherName or something.

Playground link to code