"You’re saying that any object given this contract must be an object containing 2 properties (no more, no less)"
The parenthetical 'no more, no less' should be removed here as it is misleading. TypeScript interfaces do allow the objects that 'implement' them to of course have other properties defined. See the LabelledValue example in the TypeScript docs:
Note that it's only misleading due to the ambiguous grammar:
The sentence can be read as
"You’re saying that any object given this contract must be an object containing 2 properties (the object may contain no more, no less)"
or
"You’re saying that any object given this contract must be an object containing 2 properties (you are saying no more, no less)".
"interfaces don't have a runtime representation" I was looking for a good way to say that while writing the article, this is a very good way ! Thanks
Also I like that you think about the use of instanceof, I didn't really think of that, but some devs use this condition intensively.
"instaceof" is something I would advise against for any code that might be called from another module.
If you have different modules requiring that same module and they for whatever reason (webpack behavior, node not deduping properly, npm link, different versions, etc), that instanceof will fail and the caller will be left scratching their head as to why their object fails the check.
In JavaScript (and by extension TypeScript), you are best left to duck-typing for run-time checks.
> I know it can be confusing for people coming from an OOP language, but in Javascript an object IS NOT an instance of a class. I’ve worked with C++ for about ten years, so I understand it feels right when doing
> let mine = new MyClass();
> you get an ‘object’. But when thinking that you forget that Javascript is not a ‘class based’ language, it uses a prototypal approach.
I really wish he were more specific about this. When in Javascript we write:
the language tells us that the object `foo` is indeed an "instance" created by the constructor function `Foo`. What is the essential difference between this and what we have in object-oriented languages? What are the practical implications of Javascript being "not a ‘class based’ language" that "uses a prototypal approach"?
The class system is a bit hit and miss. In practice you will be fine if you're using classes and you will be fine if you use prototypes but you should avoid mixing them directly.
Strictly speaking, there is no essential difference because it's entirely possible to implement classical inheritance using prototypal inheritance -- just not the other way around.
There's no classical equivalent of this, for example:
foo = {hello: "world"};
bar = Object.create(foo);
console.log(bar.hello); // "world"
Note how bar has no "hello" property, so the property is looked up on its prototype, foo.
Your code uses the "new" keyword. That keyword is pretty much emulating classical behaviour on top of the prototypal inheritance. The following two examples are equivalent:
Every function implicitly has a "prototype" property which is set to an object with a "constructor" property set to the function itself.
Object.create creates a new object and sets its prototype (the internal [[Prototype]] property, not the "prototype" property on the function) to its argument.
There's a bit more going on underneath (mostly to allow certain optimisations) but conceptually even the "new" keyword is just syntactic sugar.
To be clear: in practice there are three ways to use inheritance in JS:
1) Prototypes with constructors using the "new" keyword (ES3+):
function Foo (name) {
this.name = name;
}
Foo.prototype.hello = function () {
return `Hello ${this.name}!`;
};
let foo = new Foo("world");
console.log(foo.hello()); // Hello world!
2) Plain prototypes with Object.create (ES5+):
let bar = {
name: 'world',
hello: function () {
return `Hello ${this.name}!`;
}
};
let baz = Object.create(bar);
baz.name = 'Wisconsin';
console.log(bar.hello()); // Hello world!
console.log(baz.hello()); // Hello Wisconsin!
3) Classes (ES2015+):
class Qux {
constructor (name) {
this.name = name;
}
hello () {
return `Hello ${this.name}!`;
}
}
let qux = new Qux("world");
console.log(qux.hello()); // Hello world!
Note how all three examples do (roughly) the same thing but in slightly different ways.
Inheritance in #1 is a bit awkward (which is why before ES2015 there were myriads of different inheritance libraries).
In #2 it's fairly straightforward but type checks are unintuitive because there really aren't any types involved (instanceof requires a constructor).
In #3 both inheritance and type checks are fairly straightforward but it also uses more abstractions, which can make it more difficult to understand conceptually (especially when coming from a language like Java or C# which is syntactically similar).
IMO #1 is the worst of both worlds because the behaviour of the "new" keyword and the special "prototype" property are difficult to understand and the constructor looks like a regular function but expects to be called in a special way (using the "new" keyword) and will break in unexpected ways if called as a normal function:
> TypeError: Cannot set property 'name' of undefined
(or worse: outside strict mode it might not fail and actually try to write to the global scope -- so remember to always "use strict")
While the value of a class is also a constructor function, calling it without "new" will break with a distinct error that makes the mistake obvious and easy to understand:
> TypeError: Class constructor Qux cannot be invoked without 'new'
I've never used Object.create for inheritance (although I heard it was a favorite of Douglas Crockford's at some point).
And the syntactic sugar of es2015 classes is so appealing I never wrote constructor functions on a regular basis.
What's so conceptually difficult about es2015 classes that confuses people? They always felt intuitive to me. A bit restrictive perhaps, because you don't have private methods, or instance properties, and if you want to call super in a method you have to do it before anything else, but other than that — nothing especially confusing.
Yes, this puzzles me too. Maybe the fact that you can redefine both the methods of `foo` and the function `Foo` at any time? And you can also change `foo.prototype`, I guess?
I don't understand the point at the end, showing that interface definitions don't create code doesn't have anything to do with code generated for actual instantiation. Once you create an instance of something, no matter what mechanism is used, code will get created, contract enforcement has nothing to do with that.
Comparing to OOP languages I see the class definitions in the beginning of the post as "data grouping" and not as contracts so I don't quite understand what's being contrasted here.
Of course, you are right when you say "Once you create an instance of something [...] code will be created".
But what I am pointing here is BEFORE you create any instance of anything.
You define a contract, what your object should look like. With an interface it's checked before runtime and then translates to no code. But using a class, you have code generated for the runtime.
Are you concerned about code size then? That would make sense for a browser app, coming from desktop I'm not too concerned with code that gets generated but might not get called.
The comparison of the JS output is not entirely fair considering it's likely trying to generate ES5 compatible JavaScript and thus "emulating" classes rather than using the built-in classes available in ES2015 and later.
The output of Babel (which translates newer language features to older versions like ES5) is actually even more verbose because it adds some runtime error handling:
JavaScript classes build on prototypes with constructors and use prototypal inheritance under the hood, but they're more than the sum of their parts and can be a useful abstraction.
I'd argue that the article is right that in most cases it's probably cleaner in TypeScript to simply use an interface or type alias instead of a class, but although some people like (for example) Eric Elliott have very strong feelings to the contrary, classes are a useful language feature for certain scenarios and avoiding them at all costs can result in worse code than knowing when to use them.
The difference IMO is that the interface is still a nominal declaration. With it you have a named type, while the alias is purely structural. in TS/Flow I prefer to use the type alias ubiquitously, however in other typed languages I prefer being more nominal. In Purescript, which supports both methods also, I almost always fully declare the type.
TypeScript interfaces are purely structural. The effect on the type system of using `interface Foo { … }` vs `type Foo = { … }` is the same. I mean, the whole point of interfaces in TypeScript is that it's structural typing, and if you want nominal typing you use a class.
The only difference is readability and how tools like VSCode treat it (e.g. if you hover over something typed as an `interface`, VSCode just shows the interface name, though you can hold a modifier to see the whole definition).
All that said, if you want to exclusively use type aliases, you're certainly free to do so. But I expect most TypeScript developers will consider that to be a bit odd.
I used to think that classes get special treatment in typescript, because they both reference a type and a constructor function. But I recently found out that you can do the same by exporting a constructor function and it's type with the same name:
module a:
export type A = { a: string };
export const A : () => A = () => { return {a: string}; };
module b:
import A from './a';
const a = A(); // a is of type A
This way you can completely ditch classes, and still only import a single type/constructor combo. Not having to use `new` means easier composition and generally going more in the direction of functional programming.
This is in no way equal to having a 'class A { ... }' that you'd instantiate with 'new A()'. In module b, the const a will not be of 'type A', but a plain object with signature of '{ a: string }' for which A is an alias. That's very different because a plain object is not an instance - you can't use instanceof, try to console.log() it and an instance of a class and you'll see the difference.
I didn't mean that it's equal to having `class A {...}`. What I said is that you can export a type and a function with the same name, which is as convenient as importing a class, which is also both a type and a constructor function (edit: with prototype).
By "constructor function" I meant a function that constructs an object with a given shape, not a "class instance" or prototyped object. Sorry about not being clear. So no `instanceof`, but you are guaranteed to get correct type checking.
See here [1] for example, if you hover `a` it will tell you it is an `A` not a `{a:string}`.
Yes, Typescript will tell you that it's of type A, but that's not what happens at runtime - that's what I wanted to say. During runtime it's a plain object that has the aforementioned signature.
> One can agree or disagree, but like he says : “classes are nice but they are not necessary in Javascript”. I would say, since they are here and make life easier for a lot of people, you can use them for whatever reason you want
How unnecessary classes make life easier for a lot of people? It's probably one of the worst JS anti-patterns.
I am new to Typescript, but I don't quite see why and how it hurts at the end. He defines two interfaces that are not implemented in any classes, so those interfaces don't do anything to the actual program yet. I guess it is safe for Typescript to produce no code. Any specific scenario this will be problematic?
> I guess it is safe for Typescript to produce no code.
TypeScript produces no code in that case because interfaces are a TypeScript feature and are only used to check your code before compiling it down to JavaScript.
The parenthetical 'no more, no less' should be removed here as it is misleading. TypeScript interfaces do allow the objects that 'implement' them to of course have other properties defined. See the LabelledValue example in the TypeScript docs:
https://www.typescriptlang.org/docs/handbook/interfaces.html