Open tom-sherman opened 2 years ago
The differences in TypeScript are powerful and important enough to, if you "standardise" on one of the two, to either make a lesser combination of both, or have additional keywords or syntax to allow the differences to be declared.
type
, or using seal
in other languages) is a very powerful featureinterface
) is a very powerful featuretype
) is a very important feature for "eventual non-primitive" adoption, which is not possible using interface
declarationsI wouldn't dismiss how often enabling merging can help you out (with existing declarations, plugin systems, custom extension of prototypes, etc.) and how often sealing can help you out (by limiting it to the exact and single type declaration).
In TS they are mostly interchangable (aside from the differences listed in the docs Jamie linked). But in a language like flow they are not - An object type represents the type of an object literal, but an interface is used to more accurately represent a hierarchical object (eg a class).
For example - in TS you can have an interface
that extend
s an object type, or a class that implements
an object type. However in flow this is an error because object types are not inheritable.
It would be wrong to enforce TS's interchangability on the entire language.
Likewise Rust's traits and types are very different language features, and I suspect others on that list.
I think it's wise to minimise the surface area of a proposal like this both to make adoption in TC39 more likely and to reduce harm should it not pan out as a useful language feature long-term. To that end, fewer and less opinionated reserved words should be favoured. Future proposals can expand support for "types as comments".
If TypeScript et al didn't already exist, I think there'd be a strong argument for only supporting type
in JavaScript and having languages like TypeScript embed their concept of an "interface" via something like type augment
. That is assuming that JavaScript will ignore everything after type
, which is my understanding. This is also maximally flexible for these languages going forwards; for example newtypes could be implemented via type unique
.
If one were to agree with that, then the question becomes whether JavaScript should set in stone an additional keyword to suit the superset languages of today, or whether TypeScript et al should adjust. I deem the latter preferable on the basis that they're compiled languages with the luxury of being able to take advantage of breaking changes.
I also feel interface
might be redundant, especially consider that we already have first-class protocol proposal ( https://github.com/tc39/proposal-first-class-protocols ) which much closer to interface/trait/protocol/mixin in other languages.
whether JavaScript should set in stone an additional keyword to suit the superset languages of today
interface
and implements
are both reserved keywords already. So nothing new to reserve.
I deem the latter ((TypeScript et al should adjust)) preferable on the basis that they're compiled languages with the luxury of being able to take advantage of breaking changes
If you've worked in and around TS at all in the past 7 years - you would have seen that they purposely do not make many breaking changes - ESP not breaking syntax changes. Such breaking changes lock out countless users from being able to upgrade version because they need to perform expensive, high-friction codebase codemods (assuming it's even 1:1 codemoddable).
If your suggestion is instead that TS should compile down its types to suit a "no-interface" JS world; then that kind of defeats the purpose of this proposal IMO. Most of the wins from this proposal is improved DevX wherein people can run their typed-code in a JS engine without a build step. If you need to compile your TS types down so they work in JS - then you might as well just not emit any type annotations. So then what's the point of speccing this at all?
In TypeScript the generally recommended way to write an object type is using an interface, not a type alias declaration with an object literal type. This is a historical thing because in the past interfaces could be concretely named by the type system - making DevX (errors, intellisense, etc) better. So in most codebases you will find people using interfaces instead of type alias declarations.
If your suggestion is instead that TS should compile down its types to suit a "no-interface" JS world; then that kind of defeats the purpose of this proposal IMO.
We need to trade-off if this is one of the 90% features that critical to leave alone, if it is one of the 10% that is hard breaking, or if it is something that TS want's to support. Not supporting in JS leaves the current state alone, supporting it enables changing the compiler to no longer need to convert. The question is whether or not the proposal should explicitly enable it and it serves enough value as is for non-TS current developers.
@bradzacher at my recent employers, we've switched all interfaces to type
s for a number of usability reasons, and forbidden use of interface
, as a data point.
@ljharb Definitely - there are plenty of codebases that do the opposite and always use type
s instead of interface
s. Recent TS versions have improved the DevX enormously for them so they're almost on par.
In Flow you really only use type aliases, and only use interfaces if you're working with classes. In fact - interfaces are such a distinct thing in flow that there is even an inline interface syntax (type T = interface { ... }
)!
As I switch back and forth between the languages I have found that I personally prefer using type aliases for everything. It does look and feel nicer IMO to keep interfaces relegated to class usecases.
I personally would love to see TS standardise on type aliases instead of interfaces - but I also recognise that it's a massive breaking change that is really intractable for TS to force on people.
however, it's not intractable for this proposal to start out with as a helpful constraint ;-)
Agreed. I also believe also we should probably drop interface
in favor of something like type
. interface
in JS doesn't make any sense, just like class
is misleading in code already. Unfortunately, we're stuck with classes, but let's not continue to perpetuate the wrong paradigm. We should do as much as we can to promote structural type annotations (Go is a good example of this)
@luijar I don't understand how class
is misleading in code. Even though classes are mostly syntactic sugar over prototype-based functions, I think they are very useful in their own ways. I personally prefer this:
class Person {
constructor(name) {
this.name = name
}
greet() {
alert(`Hi, ${name}!`)
}
rename(name) {
this.name = name
}
}
let fred = new Person("Fred")
fred.greet()
to its pre-ES6 and classes counterpart:
function Person(name) {
if (this === window || !this) throw new TypeError("Class constructors cannot be invoked without 'new'");
this.name = name;
}
Person.prototype.greet = function () {
alert("Hi, " + this.name + "!");
}
Person.prototype.rename = function (name) {
this.name = name;
}
var fred = new Person("Fred")
fred.greet()
The one feature of classes I do miss are the fact that we can't call them. For example, it would be impossible to rewrite Date
as a class since on of its core features is the ability to call it and return a timestamp. But on other fronts, I think classes are one of the most wonderful things added to JavaScript and interfaces go with them in the same way butter goes with toast. Additionally, interface merging is a VERY useful feature as it promotes plugin-based usage. For example, Fastify allows packages to augment its interfaces to let TypeScript-based projects have proper typings and autocomplete. Without this feature, Fastify would either need to add plugin typings to its own package (which might make users think they had features that were really unaccessible), force plugins to create subclasses (not a bad idea, but it would be a nightmare with several plugins), or ask users to use fastify() as any
when creating a server, which is a terrible idea.
Yeah, @bradzacher's point is the most valid here - the type system hole which interface
fits is a real JavaScript concern that would need to be covered by any type system trying to map JavaScript. Removing the concept isn't really an option at all.
That said, removing the concept of a top level interface
keyword which could be replaced by something like type X = interface { thing: string }
or type X = open { thing: string }
could allow less syntax carved out but the point of interfaces is that it very accurately reflects how people write JS code which builds and augments on existing objects/classes. But given that interface
is already a keyword, doesn't seem too worth the effort.
I personally would love to see TS standardise on type aliases instead of interfaces - but I also recognise that it's a massive breaking change that is really intractable for TS to force on people.
I agree with this point made by @bradzacher. TypeScript has been very strict on breaking changes and I don't think TS would ask their entire userbase to switch from interface { ... }
to type = interface { ... }
just to fit in with JavaScript standards. We've reserved the interface
keyword for years and might as well use it.
Agree with these points but we shouldn't concern ourselves too much with the impacts of breaking changes in TypeScript. The proposal as it stands is going to require some breaking changes anyway.
I know there will be some breaking changes but interfaces are a key component of TypeScript. If we break conditional types or mapped types nobody will care that much. However, interfaces must be preserved as they are one of the basic foundations of TS and other typed languages.
Nobody's talking about removing them from TS; the discussion is about an alternative way to represent them in this proposal, which TypeScript could choose to add.
Oh, I get this better now. Still, I think that the attraction of not requiring a build step would be more convincing to people if we allowed simple interface declarations. I'm also wondering why we wouldn't want to support them. Interfaces are simple to implement and the word interface
has been reserved for years; why don't we just use it?
@zsakowitz because "it's a keyword" strongly implies it has runtime semantics, and this proposal explicitly has no runtime semantics.
Oh, that makes sense. I guess you could always convert a codebase by running a search for /interface\s+(\w+)/g
and replacing it with type $1 = interface
.
It would be a nice property to have JavaScript's only erasable top-level declaration start with [import|export] type
. It would be a shame if interface
ends up being the only other included...
Even if there are differences in TS that make both separately useful, should there be? The keywords mean the same thing on the surface and those differences would be arbitrary details users have to study.
Quick questions if we use type X = interface { ... }
syntax: if you're using generics with an interface, do we put the generics on the type alias
type Trackable<T> = interface {
oldValue?: T;
newValue: T;
track(cb: (newValue: T, oldValue: T) => void): Trackable<T>;
};
or the interface expression?
type Trackable = interface<T> {
oldValue?: T;
newValue: T;
track(cb: (newValue: T, oldValue: T) => void): Trackable<T>;
};
I feel that putting the generics on the type alias makes more sense because it shows where the generics come from, but it distances the type arguments from the interface itself which seems like a semantics issue.
Could this proposal standardise on one or the other?
These are two very similar features, similar enough to try to settle on a single syntax.