In TypeScript, I can define a type like this:
type Person = {
givenName: string
familyName: string
}
And I can define a constructor type for my type like this:
type PersonConstructor = {
new (givenName: string, familyName: string): Person
}
But I can't figure out the proper syntax for the constructor function implementation. The code below is invalid, and produces an error:
const NamedPerson: PersonConstructor = (givenName: string, familyName: string) => {
return { givenName, familyName }
}
Type '(givenName: string, familyName: string) => { givenName: string; familyName: string; }' is not assignable to type 'PersonConstructor'.
Type '(givenName: string, familyName: string) => { givenName: string; familyName: string; }' provides no match for the signature 'new (givenName: string, familyName: string): Person'.ts(2322)":
I can call the constructor, no problem, so the constructor type definition looks ok I think:
const me = new NamedPerson('First', 'Last');
What is the proper syntax for an implementation of PersonConstructor
?
The correct way to implement a construct signature is with a class
declaration or expression:
const NamedPerson: PersonConstructor = class {
constructor(public givenName: string, public familyName: string) { }
}
const person = new NamedPerson("John", "Doe");
console.log(person.familyName.toUpperCase()) // "DOE"
console.log(person instanceof NamedPerson); // true
In the above class
expression I've used parameter properties to make the definition a little easier, but you don't need to use this.
If you want to use an arbitrary function instead, TypeScript doesn't want to make that easy for you. ES5-style constructor functions work at runtime (where they assign properties to this
instead of returning anything) but TypeScript doesn't want to add support for that and the advice is "use class
es": see microsoft/TypeScript#2310. Returning values from your constructor also works at runtime, but TypeScript doesn't currently let you mark class constructors as producing something other than its this
type: see microsoft/TypeScript#27594 for the relevant feature request. And TypeScript can't really tell the difference between function
expressions (which are new
able) and arrow functions (which explode at runtime if you new
them, even if you never try to use this
inside them), so even if the above two issues were addressed, you'd need to be careful that your function wasn't the wrong kind of function.
Right now, handling all of this requires some kind of wrapper function with type assertions to suppress compiler warnings, and an implementation that makes arrow functions usable. Maybe like this:
function toConstructor<A extends any[], R extends object>(
fn: (...args: A) => R): new (...args: A) => R {
return class {
constructor(...args: any) {
return fn(...args);
}
} as any;
}
And then you'd use it like
const NamedPerson: PersonConstructor =
toConstructor((givenName: string, familyName: string) => {
return { givenName, familyName }
});
const person = new NamedPerson("John", "Doe");
console.log(person.familyName.toUpperCase()) // "DOE"
console.log(person instanceof NamedPerson); // false
Oh, yeah, person instanceof NamedPerson
is false
, which is confusing. That's one of the drawbacks of constructors that return things instead of assigning to this
; the prototype isn't set properly. So even if TypeScript made it easy-ish to use arbitrary functions as class constructors, they'll lead to weird behavior.
If you want to minimize problems at runtime and with the compiler, you should use classes as classes and functions as functions and not mix them together. If you've got a function then you'll have a better time just using it as a function without involving the new
operator at all:
const namedPerson = (givenName: string, familyName: string): Person => {
return { givenName, familyName }
}
const person = namedPerson("John", "Doe");
console.log(person.familyName.toUpperCase()) // "DOE"