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

Suggestion: Allow classes to implement type aliases #11207

Closed
Elephant-Vessel opened this issue Sep 28, 2016 · 23 comments
Closed

Suggestion: Allow classes to implement type aliases #11207

Elephant-Vessel opened this issue Sep 28, 2016 · 23 comments
Labels
Fixed A PR has been merged for this issue Suggestion An idea for TypeScript

Comments

@Elephant-Vessel
Copy link

Elephant-Vessel commented Sep 28, 2016

The implements-keyword in TS is used to guide the programmer of a class to ensure that the class is compatible with a certain (structural) type.

The interface-keyword in TS defines a type, that except being a regular type, can be used design-time as a tool to ensure that a class is compatible with a (structural) type.

Types other than those defined by interface or class can not be used with the implements-keyword.

I suggest to drop this constraint to allow authors of classes to use (almost*) any type to guide the implementation of a class.

* Except types consisting of primitives (string, bool, number...), as these are nominal and cannot be implemented by a class.

(Sorry if this already have been discussed somewhere, but I can't find anything.)

@Elephant-Vessel Elephant-Vessel changed the title Suggestion: Allow classes to implement types Suggestion: Allow classes to implement type aliases Sep 28, 2016
@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels Sep 28, 2016
@RyanCavanaugh
Copy link
Member

I don't see any reason not to

@mhegazy
Copy link
Contributor

mhegazy commented Sep 29, 2016

What does it mean to implement a union type?

@Elephant-Vessel
Copy link
Author

Elephant-Vessel commented Sep 29, 2016

That would mean that the programmer at design time is required to make the class implement (at least) either of the constituents of the union type, so that it will fulfill the contract of the union type.

@mhegazy
Copy link
Contributor

mhegazy commented Sep 29, 2016

Do you have a scenario in mind where this would be useful?

@Elephant-Vessel
Copy link
Author

Elephant-Vessel commented Sep 29, 2016

As classes and types are clearly separated concepts in structurally types languages, I'd personally would like to use these respective conceptual tools in encapsulated forms. I would like to be able to directly define types (via type aliases) that I can use throughout my code, without being forced to use the old-school class/type-intertwined concept of interfaces.

I don't see why it would be required for a type to be declared as either an interface or a class for us to use it as a way to guide design-time implementation of classes since TS is structural.

So for me, this is more about having a cleaner conceptual model rather than being able to technically doing this or that easier. That is important for me as a developer, it makes my brain happier.

@Elephant-Vessel
Copy link
Author

What do you guys think? Do you think it's a reasonable proposal?

@RyanCavanaugh
Copy link
Member

If the target is something we would have let you implement anyway, I don't see why not. But there may be some complexity hiding behind the scenes; we'll have to discuss first.

@aluanhaddad
Copy link
Contributor

I think the most interesting things to implement would be function types but there's no way of doing that with the classes especially given that the [[call]]able classes proposal was dropped by TC39.

Still I do find myself wanting to implement arbitrary type declarations from time to time. It would be useful.

@RyanCavanaugh RyanCavanaugh added Help Wanted You can do this and removed In Discussion Not yet reached consensus labels Oct 31, 2016
@RyanCavanaugh RyanCavanaugh added this to the Community milestone Oct 31, 2016
@RyanCavanaugh
Copy link
Member

Accepting PRs for this one -- allow the target of implements to be any type reference (e.g. no class X implements Y | Z). Best guess is that this should be relatively simple.

@NoelAbrahams
Copy link

We currently use type aliases (which I like to call type definitions) to describe the structure of object literals (JSON objects used as DTOs) and interfaces to describe, well the interface for a class:

type Foo ={ 
   value: string;
};

interface Bar {
   setFoo(foo: Foo): void;
}

class Bar implements Bar {}

I don't think permitting classes to implement a type definition is a good idea, because it breaks this clean demarcation.

When I see a type definition I can now immediately tell its purpose. But if type definitions are also being used to define behaviour then I loose that information.

@Elephant-Vessel
Copy link
Author

Elephant-Vessel commented Nov 1, 2016

@NoelAbrahams

I'm not sure I get your point. I get that you use those differently, but why? Why would you want to have any demarcation here in the first place?

Do you mean DTO's like in no behaviour? Then what do you think when you see this?

type foo = {
    bar(arg:string): void;
}

@NoelAbrahams
Copy link

Do you mean DTO's like in no behaviour?

yes.

It's something we enforce through convention. So we wouldn't write that definition that you mention, because bar will not serialise over the wire.

@Elephant-Vessel
Copy link
Author

@NoelAbrahams

Ok, but then you are basically saying that this is not a good language design change because it somewhat interferes with your convention?

@NoelAbrahams
Copy link

I'm saying that there is no compelling reason to change the status quo. The justification for the requested change also appears to be with the view to supporting a convention: converting all interfaces into type aliases.

Incidentally you may run into problems doing that because extending type aliases is a problem.

Sent from my iPhone

On 1 Nov 2016, at 22:26, Elephant-Vessel <[email protected]mailto:[email protected]> wrote:

@NoelAbrahamshttps://github.com/NoelAbrahams

Ok, but then you are basically saying that this is not a good language design change because it somewhat interferes with your convention?

You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHubhttps://github.com//issues/11207#issuecomment-257717774, or mute the threadhttps://github.com/notifications/unsubscribe-auth/ABDjh_f7DAg3o086Yklnf6rZ9PqF-htOks5q57ysgaJpZM4KIrA_.

@Elephant-Vessel
Copy link
Author

Elephant-Vessel commented Nov 1, 2016

In my head, this is a matter of making the language more coherent and less complex, not about enabling a certain convention. Since ts is structurally typed, interfaces are really nothing more than design-time support for a developer to make sure that a class fulfills a certain contract.

Types, irrespective of whether they are produced from classes, interfaces or type aliases, are used throughout the language with little discrimination. Except from the current constraint that classes only can implement types declared through interfaces.

I think these kinds of constraints on a conceptual model, that inherently make the conceptual model more complex, needs to be properly justified. I cannot see how this constraint adds any value to motivate this complexity, and I'm sure we all have enough complexity in our life as programmers to feel stimulated anyway.

I believe it's valuable to be able to think about only classes, objects and types, instead of classes, interfaces, objects and types, which you cannot do now. Using interfaces with classes could be optional instead of required.

This is something that bothers me as a developer, I'm not just throwing random suggestions out there trying to be a language designer. I think the value justifies departing from status quo.

Incidentally you may run into problems doing that because extending type aliases is a problem.

That could be expressed as an intersection type, or what do you mean?

And to avoid misunderstandings, I'm not saying that interfaces should not be used. I'm only proposing that you shouldn't be forced to use them.

@NoelAbrahams
Copy link

I'm all for a simpler conceptual model. But that is actually a function of how complex a codebase is. For the work we do, I feel it is useful to have different semantics for type aliases in order to model JSON types. I have a feature request out for a 'json' keyword #1897 but that's not getting any love ATM

That could be expressed as an intersection type, or what do you mean?

See #11961 for a difference between interfaces and type aliases.

@Elephant-Vessel
Copy link
Author

Elephant-Vessel commented Nov 3, 2016

I like the idea to have some way to strongly separate objects of pure data and objects with behavior. And I'm sure that you do agree that type aliases isn't really the right tool for that.

See #11961 for a difference between interfaces and type aliases.

I don't know... I agree it feels odd that the type of foo3 depends on the order of which the constituents of the intersection type are declared. And I also agree with the problem of interpreting what T[] & U[] actually mean, so maybe I'd rather have the compiler just not allowing those intersections in the first place.

If someone is trying to have a class implement { x: T[]} & {x : U[]}, the compiler should probably at least have a clear and consistent understanding of what that actually mean if it's allowed at all.

Interfaces gets around this problem as each extending type have to fulfill the extended type, so the compiler only have to care about the interface highest up in the chain. So interface T extends U (properties of U is a subset of the properties in T recursively) is not at all the same as T & U.

@NoelAbrahams
Copy link

NoelAbrahams commented Nov 3, 2016

@Elephant-Vessel,

I would think the following set of features and fixes would resolve some of the issues around type aliases and how they are currently used:

@mattbishop
Copy link

mattbishop commented Dec 2, 2017

I am trying to create an "experience" (not sure what to call it) that matches chai's fluent chaining, where a member can be either a function or a step to another function. I have been able to define this as a mixin as described in http://www.typescriptlang.org/docs/handbook/advanced-types.html

Identifier is my interface; basically I am showing that the type field from that interface is accessible in the resulting call to factory("hi")

type StringFactory = (value: string) => Identifier<string>;
type StringListFactory = (...values: string[]) => Identifier<string[]>;

type Factory<T extends StringFactory> = T & { list: StringListFactory };


const factory: Factory<StringFactory> = (v) => {
  // ?? How do I implement a factory:
};

const str: Identifier<string> = factory("hi");
const strList: Identifier<string[]> = factory.list("a", "b", "c");

assert(str.type === "string-type");
assert(strList.type === "string-list-type");

I'd like to be able to implement factory in some way that didn't involve merging prototypes or similar monkey-patching mechanisms.

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Dec 3, 2017

@mattbishop there isn't an elegant way currently. The closest I know of is something like

interface Identifier<T> {
    id: T
}

type StringFactory = (value: string) => Identifier<string>;
type StringListFactory = (...values: string[]) => Identifier<string[]>;

type Factory<T extends StringFactory> = T & { list: StringListFactory };


const factory: Factory<StringFactory> = function () {
    const factory = (v => ({ id: v })) as Factory<StringFactory>;
    factory.list = (...args) => ({ id: args });
    return factory;
}();

In TypeScript Playground

@mattbishop
Copy link

Thanks! I ended up following a similar pattern but had to use factory["list"]. I switched to your way and it's better. You cast the factory at the first line which allows factory.list to pass compilation.

@aluanhaddad
Copy link
Contributor

Well I use --strict so I would get an error writing factory["list"]. I think . and [] should be treated equivalently.

Anyway, it would really be nice if this could be done in a single expression.

If we had Callable Class Constructors we could write

const factory: Factory<StringFactory> = class {
    call constructor(v) {return {id: v};}
    static list(...values) {return {id: values};}
};

I'm not sure why the proposal was withdrawn. I read a note somewhere that said it was dropped because it could be accomplished using decorators instead. However, that sounds awkward.

@RyanCavanaugh RyanCavanaugh modified the milestones: Community, Backlog Mar 7, 2019
@DanielRosenwasser
Copy link
Member

I think this one's already fixed by #13604.

@DanielRosenwasser DanielRosenwasser added Fixed A PR has been merged for this issue and removed Help Wanted You can do this labels Jan 15, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Fixed A PR has been merged for this issue Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

7 participants