Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type aliases and other changes in language specification #998

Merged
merged 2 commits into from
Nov 1, 2014

Conversation

ahejlsberg
Copy link
Member

Changes in this PR:

@sparecycles
Copy link
Contributor

Could you clarify why a type is allowed to reference a union of a primitive and an anonymous type but not an anonymous type by itself?

type Text = string | { text: string };   // <-- example in spec
type Point = { x: number; y: number; };  // <-- explicitly disallowed by spec

@ahejlsberg
Copy link
Member Author

A type alias for an anonymous object type literal:

type Point = { x: number; y: number; };

would be almost the same as an interface declaration:

interface Point { x: number; y: number; }

The subtle differences include:

  • An interface is open-ended (can have multiple declarations), a type alias is not.
  • You can derive from and implement an interface, but neither is possible with an alias for an anonymous type.
  • In error messages and quick info, an interface is displayed by name whereas a type alias is displayed by structure.

We feel that the confusion caused by these differences are better avoided.

@ahejlsberg
Copy link
Member Author

I should add that an alternative to the error would be to just treat type alias declarations for object type literals as equivalent to interface declarations. We discussed doing that, but we thought it would be odd for such type alias declarations to merge with other type alias or interface declarations for the same name. But disallowing merging would then again make them different from interfaces.

Anyway, I could be persuaded we should just say type aliases for object type literals are exactly equivalent to interface declarations, merging and all.

@sparecycles
Copy link
Contributor

I understand that there are subtle semantic differences (which could be employed to create an interface type which can't be extended) but I feel we don't need to have an arbitrary restriction in order to sidestep some perceived difficulty down the line.

All I can do is urge you to reconsider.

If you don't reconsider, I can always work around the restriction by defining

type Point = { x: number; y: number; } | { x: number; y: number; };

😉

@RyanCavanaugh
Copy link
Member

We didn't really like the carve-out for object types either, but it seems better than the alternatives. Are there specific use cases where you want to write type Point = { ... } instead of interface Point { ... }? If so, which semantics do you want? Or is it just an objection to the restriction on philosophical grounds?

@sparecycles
Copy link
Contributor

Mainly philosophical, given that it's supported as part of a union.

I don't think type aliases should be extendable. In that sense type Point = { ... } should not be the same as interface Point { ... }. There's no reason to special-case the non-union case to be the same as the named interface case.

Another thing to consider is function types:

type fun = () => number; // allowed
type fun = { // not allowed?
   (): number;
   (_:string): string;
};

By disallowing the second one, the ability to declare overloaded function type aliases is restricted to exported interfaces only.

@RyanCavanaugh
Copy link
Member

The danger is that if we don't make type Point = { ... } the same as interface Point { ... }, then you can't say interface P extends Point or class P implements Point (only named types can be used in heritage clauses). It's likely that a lot of .d.ts authors won't understand the distinction and will blindly use type without realizing they're making it a pain to use the type in certain ways.

That's the same reason we allowed shorthand function types as alias targets -- implementing or extending those types would be very rare compared to object types with properties.

@sparecycles
Copy link
Contributor

I understand your reasons. I still disagree.

I have to argue that .d.ts authors would be, in general, more technical than .d.ts users, and could be trusted to use type v.s. interface effectively.

But I don't think I can contribute any more to this discussion. Thanks for your time.

@ahejlsberg
Copy link
Member Author

@sparecycles @RyanCavanaugh I do agree it is an odd restriction and one that we're not particularly fond of. I'm starting to think we should just allow it and note the differences (and their consequences) in the Language Specification.

@NoelAbrahams
Copy link

The original objection to this was that if we wanted to annotate the parameter in the following function:

function foo(point: { x: number; y: number }){}

with the introduction of type aliases, there would be three ways of annotating point:

// A. Using an interface
interface Point {
 x: number;
 y: number;
}

// B. Using typeof with type aliases
var point: {  x: number;  y: number };
type Point = typeof point;

// C. Directly declaring with type aliases
type Point = { x: number; y: number };

All three methods of declaring Point are equivalent for the _use-case outlined above_, because Point is not intended to be implemented by any class. The extensibility of Point is also not an important consideration for in-house code, because it will just be a case of changing it to an interface as required.

My specific objection is that in an organisation, when there is more than one way to accomplish the same thing, we end up having half the people using one mechanism and the other half the other. Then we have situations of people correcting each other's coding styles in an endless death spiral 😃 .

This is of course why linters exist, but I was arguing for the problem to be resolved at source.

However, in this case, since only the third mechanism is prohibited, it doesn't really achieve the desired result in any case.

My preference is for type aliases to be restricted to union and tuple types:

type u = string|number|{n: string};
type t = [number, number];

type s = string; // error

We have lived without type aliases for two years now without a significant demand from the community.

@sparecycles
Copy link
Contributor

I think we have to ask ourselves, are we adding type aliases to fill a particular need with union and tuple types, or are we extending typescript with a new feature?

@ahejlsberg
Copy link
Member Author

I think we just have to accept the overlap between interface declarations and type alias declarations. The key observation here is that an alias is _exactly_ the same thing as writing the type it aliases, and since anonymous object type literals overlap with interface declarations, then so do aliases to object type literals. At the end of the day, a lot of this comes down to the fact that we have a structural type system and that there will always be multiple ways to write a particular type.

I completely agree that it would ideal not to have this overlap. However, artificial restrictions seem like a cure that's worse than the disease, so I think we should drop them for now.

@ahejlsberg
Copy link
Member Author

Updated Type Alias section in the spec to describe differences between interfaces and type aliases for object type literals.

@CyrusNajmabadi
Copy link
Contributor

@ahejlsberg The updated section looks great!