TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

Permit type alias declarations inside a class

Open NoelAbrahams opened this issue 9 years ago • 81 comments

I normally want my class declaration to be the first thing in my file:

module foo {

  /** My foo class */
  export class MyFoo {

  }
}

So when I want to declare an alias, I don't like the fact that my class has been pushed down further:

module foo {

 type foobar = foo.bar.baz.FooBar;

  /** My foo class */
  export class MyFoo {

  }
}

I suggest the following be permitted:

module foo {

  /** My foo class */
  export class MyFoo {
    type foobar = foo.bar.baz.FooBar;
  }
}

Since type foobar is just a compile-time construct, and if people want to write it that way, then they should be permitted to do so. Note that the type foobar is private to the class. It should not be permissible to export foobar.

NoelAbrahams avatar Feb 12 '16 22:02 NoelAbrahams

What scenarios would this enable other than putting it outside the class? it is not expected to be accessible on the class instance/constructor for instance?

mhegazy avatar Feb 12 '16 22:02 mhegazy

It's just a way to deal with long-winded namespaces inside a class. No, I wouldn't expect the class to act as a scope for this.

Perhaps #2956 will solve this better. I was just (sort of) thinking aloud here.

NoelAbrahams avatar Feb 12 '16 22:02 NoelAbrahams

This looks like a things applicable within a namespace or a function. a class defines a shape of an object rather than a code container, so i would not allow a top-level declarations within the body of a class that does not contribute to the "shape" of the instance of the constructor function.

mhegazy avatar Feb 12 '16 22:02 mhegazy

Yes, a class is not a container. It's just for use within the class. I'm not realy sure that I want to defend this ? I just realised that the use case I'm looking at actually requires the type in a value position.

NoelAbrahams avatar Feb 12 '16 22:02 NoelAbrahams

You could do this, which isn't terribly great but isn't horrible either:

module foo {
  /** My foo class */
  export class MyFoo {
      x: MyFoo.foobar;
  }
  namespace MyFoo {
      export type foobar = string;
  }
}

RyanCavanaugh avatar Feb 12 '16 22:02 RyanCavanaugh

@RyanCavanaugh, I agree. That would be the best second choice. I think I've used it a couple of times, but found that positioning code at the bottom of the file a bit disconcerting. I just like the idea of having just the one class per file with no other distractions.

NoelAbrahams avatar Feb 12 '16 23:02 NoelAbrahams

The scenario should be handled by the existing type alias declarations outside the class, or using a namespace declaration as outlined in https://github.com/Microsoft/TypeScript/issues/7061#issuecomment-183517330.

Adding a declaration on the class that does not appear on the type does not fit with the current pattern. I am inclined to decline this suggestion, feel free to reopen if you have a different proposal that addresses the underlying scenario.

mhegazy avatar Feb 19 '16 22:02 mhegazy

I'm running into this pattern:

I have a generic class and would like to create a type alias that references a specialized generic type expression that is only valid for that particular class scope, for example instead of:

class GenericClass<T> {
  func1(arr: Array<{ val: T }>):  Array<{ val: T }> {
    ..
  }

  func2(arr: Array<{ val: T }>):  Array<{ val: T }> {
    ..
  }
...
}

It would be great if I could alias that complex type expression (Array<{ val: T }>) to a locally scoped type. e.g.

class GenericClass<T> {
  type SpecializedArray = Array<{ val: T }>;

  func1(arr: SpecializedArray): SpecializedArray {
    ..  
  }

  func2(arr: SpecializedArray): SpecializedArray {
    ...
  }
}

I'm not exactly sure how to effectively work around this. Both the solutions provided @RyanCavanaugh and the original one mentioned by @NoelAbrahams would still a require to parameterize the type alias. E.g:

type SpecializedArray<T> = Array<{ val: T }>;

But that's not really what I'm looking for.. The whole idea was to make it simpler and more readable.. (also, if part of the workaround meant I had to use some strange merging between a class and a namespace I would rather just have nothing at all and write all the types verbosely).

malibuzios avatar Mar 20 '16 12:03 malibuzios

@RyanCavanaugh @mhegazy

Please consider reopening this issue. Here's a copy-and-paste fragment of real-world code I'm working on that demonstrates the usefulness of a generic type captured into a locally scoped type alias to yield simpler and a more readable type:

export class BrowserDB<V> {
    ...
    set(valueObject: { [key: string]: V }, timestamp?: number): Promise<void> {
        type EntryObject = { [key: string]: DBEntry<V> };

        return PromiseX.start(() => {
            if (timestamp == null)
                timestamp = Timer.getTimestamp();

            let entriesObject: EntryObject = {};
            let localEntriesObject: EntryObject = {};

            for (let key in valueObject) {
                entriesObject[key] = {
                    timestamp: timestamp,
                    key: key,
                    value: valueObject[key]
                }

                localEntriesObject[key] = {
                    timestamp: timestamp,
                    key: key,
                    value: undefined
                }
            }
        ...

Unfortunately today I cannot capture { [key: string]: V } into a type alias that is scoped to the class and captures V, e.g instead of:

set(valueObject: { [key: string]: V }, timestamp?: number): Promise<void> {

Write:

export class BrowserDB<V> {
    ...
    type ValueObject = { [key: string]: V };

    set(valueObject: ValueObject, timestamp?: number): Promise<void> {
       ...
    }
...

malibuzios avatar Mar 24 '16 16:03 malibuzios

This could have easily been written as a type alias outside the class, without sacrificing readability or convenience.

type EntryObject<V> = { [key: string]: DBEntry<V> };
class BrowserDB<V> {
  set(valueObject: EntryObject<V>, timestamp?: number): Promise<void> {
  }
}

mhegazy avatar Mar 24 '16 16:03 mhegazy

@mhegazy

The idea is that the generic parameter is captured within the alias itself, reducing EntryObject<V> to EntryObject and cleaning up the code a bit. This is perhaps not an ideal example. There could also be situations where there are multiple generic parameters like:

EntryObject<KeyType, ValueType, MyVeryLongClassName>

etc. and then the impact on the readability of the code would be more apparent.

The fact that I did not provide an example with multiple and long named type parameters does not mean that people aren't encountering this, and couldn't benefit from having this in some cases. My example was honest as I simply don't have much code that uses many type parameters and longer name for the type name provided.

Another advantage is that having the alias as local to the class would not 'contaminate' a larger scope, that may have usage of a similar alias. That seems like a basic design principle of type aliases, so it is not really applied to its fullest here.

malibuzios avatar Mar 24 '16 16:03 malibuzios

Still do not see that this warrants breaking the consistency of a class enclosure as noted in https://github.com/Microsoft/TypeScript/issues/7061#issuecomment-183514814.

mhegazy avatar Mar 24 '16 17:03 mhegazy

@mhegazy

First, thank you for taking note of my request to at least try to reconsider this.

I've read the comment you linked but did not completely understand it. I'm not sure why the concept of a type declaration should be seen as bounded by Javascript scoping and as this is purely a TypeScript meta-syntax that does not relate to any run-time behavior, and removed when code is compiled.

I'm afraid I'm not sufficiently familiar with the compiler code and the intricacies of the design here, but that's basically all I can see from my viewpoint.

malibuzios avatar Mar 24 '16 17:03 malibuzios

it is not the compiler. it is more of a question of syntax consistency.

Declarations inside a class body are only constructor, index signature, method, and property declarations. A class body can not have statements, or other declarations. all declarations inside a class body "participate" in the shape of the class either instance or static side. also all declarations inside the class are accessed through qualification, e.g. this.prop or ClassName.staticMethod.

having said that, i do not see a type alias declaration fit with this crowd. and allowing it would be fairly odd. there is value in consistency and harmony in the a programming language syntax, and i do not think the convenience of not writing a type parameter warrants breaking that.

mhegazy avatar Mar 24 '16 19:03 mhegazy

@mhegazy

I actually see it as very natural and it is very strange (surprising?) for me to hear it described this way. I think I have a strong conceptual separation between run-time syntax and meta-syntax. I see no problem, functional or aesthetic in having types in this context!

I also see a generic type scoping pattern:

class GenericClas<T, U> {
}

Now, since T and U are types and scoped to the class. Your sort of view would look at this and say, "classes shouldn't have generic parameters because generic parameters do not really fit with the class concept": they are not accessed with this etc.

Since generic parameters are type "aliases" in a broad sense, I see no conceptual difference between them and class-scoped type aliases in this context. They are very similar in the sense that both are notation for a type that is scoped only to the class. It looks very natural for me that aliases would be possible in this particular scope. In any case, since there are already types that can be scoped to a class, adding more does not seem to me to introduce anything novel or even special to me.

Anyway, If the TypeScript is not interested in having class-scoped type aliases, then I guess there's nothing I can do. It's your language and you are the ones who are responsible for it and get paid to improve it. The only thing I can say is that the reasoning here seems very subjective and somewhat arbitrary.

malibuzios avatar Mar 24 '16 20:03 malibuzios

related #2625

consider the following hack that enables what you want (not exactly in a class but very close to it)



export function toInstanceOfMyClassOf<A, B>() {

    type C = A | B;

    return new class {
       public doThis(one: A, another: B) {
       }
       public doThat(value: C) {
       }
    };

}

zpdDG4gta8XKpMCd avatar Apr 13 '16 18:04 zpdDG4gta8XKpMCd

+1

huan avatar Mar 15 '18 14:03 huan

Here's an example where this feature would be very useful.

export class State<Constant, Trigger> {
  private _transitions: Map<Trigger,
                            Transition<State<Constant, Trigger>, Trigger>>;

  constructor(public value: Constant) {
    this._transitions = new Map<Trigger,
                                Transition<State<Constant, Trigger>, Trigger>>();
  }

could become

export class State<Constant, Trigger> {
  type TransitionT = Transition<State<Constant, Trigger>, Trigger>;
  type MapT = Map<Trigger, TransitionT>;

  private _transitions: MapT;

  constructor(public value: Constant) {
    this._transitions = new MapT();
  }

I'm sorry to see this proposal was declined because a good example was not provided on time. I hope this will be reconsidered.

sztomi avatar Mar 21 '18 21:03 sztomi

+1

InExtremaRes avatar Mar 23 '18 18:03 InExtremaRes

The provided example is actually pretty illuminating - type aliases currently can't close over other type parameters. Is the intent that you could (outside the class body) refer to State<Foo, Bar>.MapT ?

RyanCavanaugh avatar Mar 23 '18 20:03 RyanCavanaugh

@RyanCavanaugh look what you made me do: https://github.com/Microsoft/TypeScript/issues/18074

zpdDG4gta8XKpMCd avatar Mar 23 '18 20:03 zpdDG4gta8XKpMCd

@RyanCavanaugh Yes, exactly (and I think the possibility to avoid repetition improves the robustness of generic code like this: if I change the definition of MapT that change will carry over to wherever it's used).

sztomi avatar Mar 23 '18 20:03 sztomi

Today classes don't introduce a namespace meaning, and namespaces are always spellable with bare identifiers and don't require the possibility of type parameters. So this would be really quite a large change to just add sugar to do something you can already do today with a namespace declaration and slightly more type arguments.

RyanCavanaugh avatar Mar 23 '18 21:03 RyanCavanaugh

Does that also help with repetition?

sztomi avatar Mar 23 '18 21:03 sztomi

This is really annoying not having this. Please check out C++ using syntax - simple and wonderful.

Viatorus avatar Mar 27 '18 19:03 Viatorus

Here's an example I keep running into:

type StateMachineState = 'idle' | 'walking' | 'running' | 'flying' | 'swimming';
class StateMachine {
  private state: StateMachineState = 'idle';
  ...
}

In this case, I would like to fully encapsulate the use of states within StateMachine. The StateMachineState type should not be available to other code because it is an implementation detail of StateMachine. Currently, the only way to totally hide it (without using a namespace) is to do this:

class StateMachine {
  private state: 'idle' | 'walking' | 'running' | 'flying' | 'swimming' = 'idle';
  ...
}

...which is quite ugly, and I lose the ability to re-use the union type in some internal StateMachine method later on. I would instead prefer to be able to write it like this:

class StateMachine {
  private type State = 'idle' | 'walking' | 'running' | 'flying' | 'swimming';
  private state: State = 'idle';
  ...
  private foo(stateName: State) { ... }
}

I am aware of namespaces, but I think they're overkill for a single class.

jcmoyer avatar Apr 13 '18 15:04 jcmoyer

Here is also an example for React setState where alias would be handy: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/25055/commits/38c9802705380d607a030b1240f76f39581977ea#diff-96b72df8b13a8a590e4f160cbc51f40cR289

So instead of

class Component<P, S> {
  setState<K extends keyof this["state"] | keyof S>(
    state: ((prevState: Readonly<this["state"]> | Readonly<S>, props: Readonly<P>) => (Pick<this["state"], K> | Pick<S, K> | this["state"] | S | null)) | (Pick<this["state"], K> | Pick<S, K> | this["state"] | S | null),
    callback?: () => void
  ): void;

we could have

class Component<P, S> {
  type SetStateSignature = Pick<this["state"], K> | Pick<S, K> | this["state"] | S | null
  setState<K extends keyof this["state"] | keyof S>(
    state: ((prevState: Readonly<this["state"]> | Readonly<S>, props: Readonly<P>) => SetStateSignature) | SetStateSignature,
    callback?: () => void
  ): void;

Of course aliasing a reference to this is impossible outside of the class scope.

Ignore the invalid code please, focus on idea.

ackvf avatar Apr 23 '18 16:04 ackvf

In my case I have types (which I later use to construct a type union) which all relate to a method, like this:

class Something {
    type A = { type: 'a' };
    methodA() {}

    type B = { type: 'b' };
    methodB() {}

    type C = { type: 'c' };
    methodC() {}

    type Union =
        | A
        | B
        | C
        ;
}

Obviously the above is the preferred syntax, not something we can do today, but I think we should be able to. As for whether the class name should contribute to the type namespace: nope. Just hoist the types out of the class, do not change anything aside from allowing us to place types into classes so they can be next to the related methods when dealing with scenarios like mine.

TomasHubelbauer avatar Jun 05 '18 17:06 TomasHubelbauer

Probably not a good solution, but you can do it this way:

class A<T, InternalT = {internalProp: number} & T> {
  public data: InternalT[];
}

tplk avatar Jun 28 '18 22:06 tplk

If you have a problem with putting it into the class body then don't do it. Consider the following:

export class State<Constant, Trigger>
    alias TransitionT of Transition<State<Constant, Trigger>, Trigger>,
    alias MapT of Map<Trigger, TransitionT>
{
    private _transitions: MapT;
  
    constructor(public value: Constant) {
      this._transitions = new MapT();
    }
    
    // ...
}

with inheritance:

export class StringMap<ValueType>
    alias BaseMap of Map<string, ValueType>
    extends BaseMap
{
    constructor(base: BaseMap) {
        // build from base
    }
}

Im not really happy with the alias word there. Maybe something like with type would work better.

Wayofthesin avatar Aug 02 '18 10:08 Wayofthesin