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 #957

Merged
merged 9 commits into from
Oct 28, 2014
Merged

Type aliases #957

merged 9 commits into from
Oct 28, 2014

Conversation

ahejlsberg
Copy link
Member

Type aliases allow names to be associated with arbitrary types.

type StringOrNumber = string | number;
type Text = string | string[];
type Point = [number, number];
type NameLookup = Dictionary<string, Person>;
type Callback = (data: string) => void;
var fullName: {
    first: string;
    middle: string;
    last: string;
};
type Name = typeof fullName;

Type aliases are particuarly useful for naming types that are not object types, such as union types or primitive types.

Type aliases can alias instantiations of generic types (as in the NameLookup type above), but they cannot themselves have type parameters.

Just like interface declarations, type aliases declare only types and not values. For example:

class List<T> {
    // List implementation
}
type StringList = List<string>;
var x: StringList = new StringList();  // Error, cannot find name 'StringList'

An error occurs on new StringList() because there is no constructor function named StringList. Only a type by that name exists. To get both, a subclass can be declared:

class List<T> {
    // List implementation
}
class StringList extends List<string> { }
var x: StringList = new StringList();  // Ok

@RyanCavanaugh
Copy link
Member

Compiling this file with --d gives the odd error "Exported variable 'p' has or is using private name 'Window'"

module M {
    type W = Window|string;

    export module N {
        export class Window { }
        export var p: W;
    }
}

@yahiko00
Copy link

I've translated this specification draft into French here: http://bit.ly/1wvcqyD

@CyrusNajmabadi
Copy link
Contributor

Are interfaces effectively a type alias to an object type then? i.e. is there any different between:

interface I { foo: number }   and
type I = { foo: number }

Also, because the right side is a type, i feel that a type annotation would be a better syntax. i.e.:

type I: { foo: number}

Finally, are type aliases allowed inside classes/interfaces? For example, can i do:

class Foo<T> {
    type ElementCollection = Array<T>;
    public foo(c: ElementCollection) { }
}

If we don't allow this, then it's not possible to create a type alias to a type that contains a type parameter (which seems unfortunate).

?

@danquirk
Copy link
Member

One difference is that as currently implemented the interface I would be emitted in a .d.ts while the type alias I would not.

Type aliases can't be parameterized so you'd have no way to say what T is for ElementCollection. Also you've written a type alias for the type Array<T> which is the static side, not the constructor function type, so you couldn't instantiate ElementCollection.

@CyrusNajmabadi
Copy link
Contributor

@danquirk Good points.

Is there a good reason though to not emit aliases in the .d.ts? Presumably this helps with code clarity on the implementation side, and i would think you would want that in the .d.ts side as well.

I definitely get the issue with being unable to instantiate type aliases. However, i still think they would be useful in a class/interface setting, esp. when describing large and unwieldy types (esp those that use type parameters and uninions).

@danquirk
Copy link
Member

I tend to agree on the .d.ts front. If you wrote them for clarity in your own code you likely want them in your API, people already use interfaces and classes this way for various things (ex naming certain call signatures). The other difference that I didn't mention before is the original motivation where you can't use an interface to name a set of union types.

On the second point, you can use type aliases for instantiated generics, just not open ones. We discussed it for awhile in the design meeting, it wasn't clear there was a great solution for something that does have a workaround (just use extends). But it's possible there's something there.

@ahejlsberg
Copy link
Member Author

@CyrusNajmabadi From a semantic point of view there is no difference between an interface and a type alias for a similar object type (or any other structurally identical type for that matter). You will however see different behavior in error messages and quick info because, just like import aliases, type aliases are always "dereferenced" to the aliased type, whereas interfaces are displayed by their name. One way to think of it is that type aliases are just names for (possibly anonymous) types, whereas interfaces are a kind of type (that always has a name).

I don't think using type annotation syntax (i.e. colon) is better. We're not saying that a type alias is something whose type is blah, where saying that it is an alias for blah. I think equals expresses that better and we already use it for import aliases.

Once we implement local type declarations in functions and methods (which we need to support ES6 style local classes) you'll be able to reference type parameters. Whether to also allow local type declarations in class and interface bodies is a different question. Not sure about that.

@ahejlsberg
Copy link
Member Author

BTW, another difference between interfaces and type aliases is that interfaces support merged declarations whereas type aliases do not.

@JsonFreeman
Copy link
Contributor

Regarding local types in class and interface bodies: I would think this is only useful if you have a lot of private methods in your class, and they would benefit from the presence of a utility type. For example:

class C {
     interface PrivateType {
         // stuff
     }

     private doStuff1(p: PrivateType): PrivateType {
         ...
     }
     private doStuff2(p: PrivateType): PrivateType {
         ...
     }
     public publicMethod(p: PrivateType) {  // Error in --declaration mode
          var p2: PrivateType; // ok inside the method
     }
}

The idea is kind of like an existential type. The class knows about a type, that others need not know about, but it is useful to defined it internally to be shared by all its private methods.

@JsonFreeman
Copy link
Contributor

Code looks good, but let's finish addressing the remaining design questions before taking this.

@ahejlsberg
Copy link
Member Author

Just pushed some changes to add support for type aliases in .d.ts files.

I think we need @sheetalkamat to look at the issue @RyanCavanaugh points out above. Best I can tell the .d.ts generation code makes assumptions about scopes and visibility of named types that can be circumvented with type aliases. But a deeper issue is that when a declaration has a type annotation, the .d.ts generator doesn't emit what was in the annotation but rather emits from the resolved Type object. The latter is much harder to get right and doesn't "see" type aliases because they have already been resolved to the aliased type. For example:

module M {
    export type W = number | string;
    export module N {
        export var p: W;
    }
}

generates the following .d.ts file:

declare module M {
    type W = string | number;
    module N {
        var p: string | number;
    }
}

Note that the type alias has been unrolled to the aliased type. I don't think we want that. Instead, when type annotations are present we should always emit them exactly as written, and we should only emit from Type objects when we're dealing with inferred types.

@NoelAbrahams
Copy link

I think the introduction of type aliases is unfortunate, since we now have two ways of declaring types:

var fullName: {
    first: string;
    middle: string;
    last: string;
};
type Name = typeof fullName;

// Same thing
interface Name {
    first: string;
    middle: string;
    last: string;
};

Why not just cover the new scenarios introduced by union types and tuples using interfaces?

interface StringOrNumber extends string | number {
}

interface Point extends [number, number] {
}

@RyanCavanaugh
Copy link
Member

Why not just cover the new scenarios introduced by union types and tuples using interfaces?

There's actually a subtle distinction here. Interfaces are always "object types" (never primitives or union types), and declaring an extends clause on an interface only copies down the members of the base type.

Allowing an interface to extend a primitive type or a union type would open up a very weird can of worms. We'd have to drastically change what extends actually means, because primitives don't have any members at all (only their apparent types do), and union types only have the members that their component types have in intersection (which is the empty set in many cases).

@ahejlsberg
Copy link
Member Author

@NoelAbrahams I think you have a valid point, but as @RyanCavanaugh points out, primitive types and union types aren't object types and it simply isn't meaningful to extend them. The best we could do is to use the interface keyword to both declare interfaces and type aliases using either extends or =. For example:

interface Name {
    first: string;
    middle: string;
    last: string;
}
interface StringOrNumber = string | number;

I'm sort of on the fence about it. I like that there isn't a new keyword, but I find it a bit odd to use interface to declare something that isn't an object type. Still, that may be an immaterial distinction for most users.

@yahiko00
Copy link

Just as a suggestion, maybe replacing interface by type keyword:

type Name {
    first: string;
    middle: string;
    last: string;
}
type StringOrNumber = string | number;

@ahejlsberg
Copy link
Member Author

@yahiko00 Since we obviously can't take away the interface keyword that would still leave us with two keywords with overlapping capabilities.

@NoelAbrahams
Copy link

@ahejlsberg

a bit odd to use interface to declare something that isn't an object type. Still, that may be an immaterial distinction for most users

that may very well be true. It's not something that seems to be terribly wrong IMO.

Also, since the following is possible:

interface MyString extends String {
}

var foo : MyString = 'foo'; // okay

why not

interface StringOrNumber extends String | Number {
}

var foo : StringOrNumber = 'foo';

@ahejlsberg
Copy link
Member Author

@NoelAbrahams It simply isn't meaningful to extend a union type, so we'd have to require the { } body of such a declaration to always be empty. That would be very confusing.

@JsonFreeman
Copy link
Contributor

@NoelAbrahams In your example, if we keep the current meaning of extends, but allow it on union types, StringOrNumber would be equivalent to {}

@RyanCavanaugh
Copy link
Member

Documenting the endpoint of the very long conversation we had today spurred by @NoelAbrahams' suggestion of not adding a new keyword. Our approach was basically to talk only about the desired semantics of type aliases, and then decide the keyword based on what the behavior of those rules were.

First, we want the following to be valid (disregard whether we use type or interface at this point):

  • type u = string|number|{n: string};
  • type g = Array<number>;
  • type t = [number, number];
  • type s = typeof SomeClass;
  • type p = string;
  • type r = Date;
  • type f = () => void;

We do not want this to be valid.

  • type o = { n: number; };

The reason for not wanting the last declaration is that someone might write o above, and then someone else would (quite reasonably) want to write interface S extends o { ... }. The type system restricts the types in a heritage clause to be named object types, and o would not be a named type (it is an alias for an anonymous type). It would be very annoying to have .d.ts authors or other people writing type o = { body }; when they should have written interface o { body; }. Same goes for re-opening the type o - we would not want to disallow this simply because of the syntax it was written in.

The proposal we ended up with is that a type alias is only a valid declaration if it it takes one of the forms listed above (or an equivalent parenthesization thereof). We briefly discussed making type o = { body } a synonym for interface o { body }, but rejected this as we do not want two ways of doing the exact same thing.

Other basic things to note:

  • Type aliases may not be 'reopened' the way interfaces can
  • You may extend or implement a type alias only if its referent is a named object type (g or r above)
  • Error messages will show the resolved type of a type alias
  • .d.ts generation will preserve type aliases if they are written explicitly in a type annotation (but not if they are the result of an inference)

@JsonFreeman
Copy link
Contributor

@RyanCavanaugh's comment:
".d.ts generation will preserve type aliases if they are written explicitly in a type annotation (but not if they are the result of an inference)"

The type alias would also have to be in scope at the place we want to refer to it. If not, we may emit a privacy mismatch error.

@ahejlsberg
Copy link
Member Author

Based on our discussion I just pushed a change that makes it an error to alias an object type literal:

type Point = {  // Error: Aliased type cannot be an object type literal. Use an interface declaration instead.
    x: number;
    y: number;
};

We decided to stick with the type keyword as we don't like the confusion that arises from using interface to declare types that aren't interfaces.

@danquirk
Copy link
Member

What's the rationale for not emitting the alias if it was inferred?

@NoelAbrahams
Copy link

👍 thanks for considering the point. I think the proposed solution is a decent compromise.

A clarification: in permitted case s:

type s = typeof X;

is the type X (although it does say "SomeClass" above) permitted to be a var?

@ahejlsberg
Copy link
Member Author

@NoelAbrahams The typeof operator itself is not affected by the introduction of type aliases and functions just like it does today. So, yes, the argument can be a var.

@yahiko00
Copy link

This is probably a stupid question but are two named types sharing a same definition compatible?
For instance

type A = number[];
function fct(a: A) { ... }

type B = number[];
var b:B;
...
fct(b); // is it a valid call?

@RyanCavanaugh
Copy link
Member

are two named types sharing a same definition compatible?

Yes. Type aliases won't affect compatibility in any way.

@NoelAbrahams
Copy link

@ahejlsberg,

Does permitting typeof mean that there is now a new way to declare types:

// using the type keyword
var point: {
    x: number;
    y: number;
}

type Point: typeof point;

// using interfaces
interface Point {
    x: number;
    y: number;
}

The role of Point here is that of a JSON object - and no class is actually expected to implement it.

@ahejlsberg
Copy link
Member Author

@NoelAbrahams The new thing is that you can declare aliases for types (but the aliases aren't actually a new kind of type). In your example above, referencing the alias Point is exactly the same as writing typeof point, and typeof point is what you'll see in error messages, IDE quick info, etc. And, because Point is an alias for typeof point, it isn't possible to use it in an extends or implements clause.

@NoelAbrahams
Copy link

Yes, thanks. That's quite clear now.

I suppose for completeness the syntax needs to accommodate all cases, but from the perspective of how we write our code I think we might just discourage use of the following cases:

type s = typeof SomeClass;
type p = string;
type r = Date;

since it doesn't make sense to call a spade anything but 😃

ahejlsberg added a commit that referenced this pull request Oct 28, 2014
@ahejlsberg ahejlsberg merged commit 16a79c5 into master Oct 28, 2014
@ahejlsberg ahejlsberg deleted the typeAliases branch October 28, 2014 22:34
@DanielRosenwasser DanielRosenwasser mentioned this pull request Nov 4, 2014
@microsoft microsoft locked and limited conversation to collaborators Jun 18, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants