Assume the following code:
interface Foobar {}
class Foo implements Foobar {}
class Bar implements Foobar {}
const arr: Foobar[] = [Foo, Bar];
This compiles, but isn't really true. typeof Foo
or typeof Bar
is Function
- makes sense, since they haven't been instantiated so far.
Is there a better way in storing non-instantiated classes than arr: any[]
?
答案 0 :(得分:3)
The empty interface here is throwing you off; this code only "works" because the interface has no members (and thus means "anything"):
interface Foobar { x: number; }
class Foo implements Foobar { x: number; }
class Bar implements Foobar { x: number; }
// Error
const arr: Foobar[] = [Foo, Bar];
The general type of a class is { new(...args: any[]): any }
if you're talking about something that can be invoked with new
, or { new(...args: any[]): Foobar }
if you're talking about something that returns a Foobar
when invoked with new
.
答案 1 :(得分:1)
interface Foobar {}
class Foo implements Foobar {}
class Bar implements Foobar {}
const arr: Foobar[] = [Foo, Bar];
This compiles, but isn't really true. typeof Foo or typeof Bar is Function - makes sense, since they haven't been instantiated so far.
Classes are values. Instances are values. There is no such thing as an uninstantiated class.
It is highly useful to be able to store classes, not just their instances.
Furthermore, as classes are just functions, the ability to do so is a natural consequence, a powerful feature we get for free.
Is there a better way in storing non-instantiated classes than arr: any[]?
As to the type of an array of them, yes you have better options than any[]
. Definitely do not use any[]
for this as the types are very precise and also easy to infer
The simplest way is just to let the compiler infer the type. This is highly precise and easy to write (just code to remove, not add)
const arr = [Foo, Bar];
Another approach is to use a tuple type
const arr: [typeof Foo, typeof Bar] = [Foo, Bar];
And finally you can use an of a union type explicitly
const arr: Array<typeof Foo | typeof Bar> = [Foo, Bar];
As for accidentally using the class, forgetting to new
it, Ryan Cavanaugh's answer covers that perfectly.
Responding to the comment remark
This would make usage of interfaces kinda useless, wouldn't it?
An interface
is a declaration used to describe the type of any value.
TypeScript is a structural typed (AKA duck typed) language. This means that all values implement interfaces simply by having compatible members.
The implements
keyword is always optional in class definitions. It is most often used to express intent to help both tools and other programmers understand the code better, but it does not change the actual types.
You are possibly thinking of interface
dispatch, hence the type FooBar[]
in your example, but in TypeScript, as in JavaScript, polymorphism is achieved via completely different techniques.
Here is an example of using an interface to describe a class instance:
interface I {
p: string;
m(): void;
}
class A implements I {
p = 'implements I';
m() {console.log(this.p);}
}
const a = new A;
class B {
p = 'also implements I';
m() {console.log(this.p);}
}
const b = new B;
const c = {
p: 'also implements I',
m() {console.log(this.p);}
};
const is: I[] = [a, b, c];
The interface I
above describes the shape of instances of the class A
, the shape of instances of instances of the class B
, and the shape of object c
.
Given this, it is natural that classes themselves can also implement interfaces.
Such an interface describes the shape of the class value itself, not its instances.
Recall that classes in JavaScript are simply functions that can be "newed" (more technically [[Construct]]
ed).
Given that, the interface representing an arbitrary class could be written
interface Newable<T> {
new(...args: any[]): T;
}
This is sometimes referred to as the type of static-side of the class as it allows for describing static
members in the interface.
interface Deserializable<T> {
fromJSON(json: string): T;
}
interface NewableDeserializable<T> extends Newable<T>, Deserializable<T> {}
Now consider the following interface and classes.
interface Character {
position: {x: number, y: number, z: number};
name: string;
}
class Snake implements Character {
position = {x: 0, y: 0, z: 0};
name = 'Snake';
static fromJSON(saved: string) {
const mc = new this();
return Object.assign(mc, JSON.parse(saved));
}
}
interface NewableDeserializable<T> extends Newable<T>, Deserializable<T> {}
const deserializableMainCharacterClass: NewableDeserializable<Character> = Snake;
The type annotations on the variable are optional but demonstrate that the class itself implements the interface because they typecheck.
Finally, this can all be done in one expression:
const Meryl: NewableDeserializable<Character> =
class implements Character {
name = 'Meryl';
position = {x: 1, y: 1, z: 1};
static fromJSON(saved: string) {
const mc = new this();
return Object.assign(mc, JSON.parse(saved));
}
};