-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Permit type alias declarations inside a class #7061
Comments
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? |
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. |
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. |
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. |
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, 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. |
The scenario should be handled by the existing type alias declarations outside the class, or using a namespace declaration as outlined in #7061 (comment). 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. |
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 ( 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). |
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 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> {
...
}
... |
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> {
}
} |
The idea is that the generic parameter is captured within the alias itself, reducing 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. |
Still do not see that this warrants breaking the consistency of a class enclosure as noted in #7061 (comment). |
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 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. |
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. 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. |
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 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. |
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) {
}
};
} |
+1 |
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. |
+1 |
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 |
@RyanCavanaugh look what you made me do: #18074 |
@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 |
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 |
Had to re-implement the long types dues to a lack of type aliasing in classes. See microsoft/TypeScript#7061
I can write like this, but I feel ugly :( export const ClassA =(function<T extends {s1:string, n1:number}>() {
type T2=Pick<T, 's1'>
return class {
t2:T2 ={s1:'123'};
}
})()
const classA =new ClassA();
console.log(classA.t2) |
This is an ugly piece of code, and a good reason to implement type aliases or equivalent for classes and interfaces, as it is now implemented for functions: export interface Selector<TContext, TMetadata, TConstruction> {
validate(parser: Parser<TContext>): SelectorValidation<TContext, TMetadata, TConstruction>
revalidate(validation: PartialSelectorValidation<TContext, TMetadata, TConstruction>, parser: Parser<TContext>): SelectorValidation<TContext, TMetadata, TConstruction>
construct(validation: CompleteSelectorValidation<TContext, TMetadata, TConstruction>, parser: Parser<TContext>): TConstruction
onEndOfStream?(validation: PartialSelectorValidation<TContext, TMetadata, TConstruction>, parser: Parser<TContext>): SelectorValidation<TContext, TMetadata, TConstruction>
} Imagine the pain of implementing this interface inside a class. |
Actually, the following are different versions of the same code, implementing the previous interface; see by yourself. Plain, painfull implementationexport class ChainSelector<TContext, TSelectorMap extends SelectorMap<TContext>>
implements Selector<TContext, ChainSelectorMetadata<TSelectorMap>, ChainSelectorConstruction<TSelectorMap>> {
public constructor(chain: TSelectorMap) {
throw Error('unimplemented')
}
public validate(parser: Parser<TContext>): SelectorValidation<TContext, ChainSelectorMetadata<TSelectorMap>, ChainSelectorConstruction<TSelectorMap>> {
throw Error('unimplemented')
}
public revalidate(validation: PartialSelectorValidation<TContext, ChainSelectorMetadata<TSelectorMap>, ChainSelectorConstruction<TSelectorMap>>,
parser: Parser<TContext>): SelectorValidation<TContext, ChainSelectorMetadata<TSelectorMap>, ChainSelectorConstruction<TSelectorMap>> {
throw Error('unimplemented')
}
public construct(validation: CompleteSelectorValidation<TContext, ChainSelectorMetadata<TSelectorMap>, ChainSelectorConstruction<TSelectorMap>>,
parser: Parser<TContext>): ChainSelectorConstruction<TSelectorMap> {
throw Error('unimplemented')
}
} Implementation using template assignationThis is what leverages a bit the pain today, but please note the constraint repetitions in the form export class ChainSelector<TContext, TSelectorMap extends SelectorMap<TContext>,
TMetadata extends ChainSelectorMetadata<TSelectorMap> = ChainSelectorMetadata<TSelectorMap>,
TConstruction extends ChainSelectorConstruction<TSelectorMap> = ChainSelectorConstruction<TSelectorMap>,
TSelectorValidation extends SelectorValidation<TContext, TMetadata, TConstruction> = SelectorValidation<TContext, TMetadata, TConstruction>,
TPartialSelectorValidation extends PartialSelectorValidation<TContext, TMetadata, TConstruction> = PartialSelectorValidation<TContext, TMetadata, TConstruction>,
TCompleteSelectorValidation extends CompleteSelectorValidation<TContext, TMetadata, TConstruction> = CompleteSelectorValidation<TContext, TMetadata, TConstruction>,
>
implements Selector<TContext, TMetadata, TConstruction> {
public constructor(chain: TSelectorMap) {
throw Error('unimplemented')
}
public validate(parser: Parser<TContext>): TSelectorValidation {
throw Error('unimplemented')
}
public revalidate(validation: TPartialSelectorValidation, parser: Parser<TContext>): TSelectorValidation {
throw Error('unimplemented')
}
public construct(validation: TCompleteSelectorValidation, parser: Parser<TContext>): TConstruction {
throw Error('unimplemented')
}
} Implementation using voodoo type aliases in template listThis is what I think it could look like with type aliases: export class ChainSelector<TContext, TSelectorMap extends SelectorMap<TContext>,
type TMetadata = ChainSelectorMetadata<TSelectorMap>,
type TConstruction = ChainSelectorConstruction<TSelectorMap>,
type TSelectorValidation = SelectorValidation<TContext, TMetadata, TConstruction>,
type TPartialSelectorValidation = PartialSelectorValidation<TContext, TMetadata, TConstruction>,
type TCompleteSelectorValidation = CompleteSelectorValidation<TContext, TMetadata, TConstruction>,
>
implements Selector<TContext, TMetadata, TConstruction> {
public constructor(chain: TSelectorMap) {
throw Error('unimplemented')
}
public validate(parser: Parser<TContext>): TSelectorValidation {
throw Error('unimplemented')
}
public revalidate(validation: TPartialSelectorValidation, parser: Parser<TContext>): TSelectorValidation {
throw Error('unimplemented')
}
public construct(validation: TCompleteSelectorValidation, parser: Parser<TContext>): TConstruction {
throw Error('unimplemented')
}
} My first thought about this feature was to allow type and interfaces to be declared inside class and interfaces as within functions, but experimenting I found that the voodoo type alias syntax is much more powerfull and has further advantages:
Consider exporting type aliasesIt would still be interesting to have the capability to export some of those aliases with something like The reason for that is that the developer could have a lot less pain as to use them. If it's a pain writing the full type inside the class, chances are it's gonna be the same using it. const chainSelector = new ChainSelector<MyContext, MySelectorMap>(someArbitraryChain)
// With exported type aliases, kewl
let chainValidation: (typeof chainSelector).TChainValidation
// Without exported type aliases, ewwww
let painfullChainValidation: SelectorValidation<TContext, ChainSelectorMetadata<TSelectorMap>, ChainSelectorConstruction<TSelectorMap>> Side note, about templated modulesThe reason I have so much template arguments inside my code is that I'm writing a generic parser. While in OCAML I would have written it using a Functor (which is no more than a templated module) I have not been able to find a way to reproduce this powerful feature in TypeScript, which I have no doubt would have reduced the list of template arguments furthermore. |
My workaround, which might be similar to what you are doing, @Kyasaki : class Inner<
T,
SomeDerivedType = Record<string, T>
> {
doSomething(p: SomeDerivedType) {
console.log(p)
}
}
export class PublishedClass<T> extends Inner<T> {}; So, the |
RationaleAfter further tries, I found that the template assignation workaround cannot cover all cases. I'm currently fighting with several error codes which ruin any effort to emulate type aliases for classes, as templates arguments are not the exact alias type, but rather extend it. All the following samples will run fine, but whatever I tried, TypeScript is crying and me too: ContextBe export type SelectorValidation<TContext, TMetadata, TConstruction> =
| InvalidSelectorValidation
| PartialSelectorValidation<TContext, TMetadata, TConstruction>
| CompleteSelectorValidation<TContext, TMetadata, TConstruction>
export enum SelectorValidationKind {
invalid,
partial,
complete,
}
export interface InvalidSelectorValidation {
kind: SelectorValidationKind.invalid
}
export interface PartialSelectorValidation<TContext, TMetadata, TConstruction> {
kind: SelectorValidationKind.partial
selector: Selector<TContext, TMetadata, TConstruction>
selection: Selection
metadata: TMetadata
}
export interface CompleteSelectorValidation<TContext, TMetadata, TConstruction> {
kind: SelectorValidationKind.complete
selector: Selector<TContext, TMetadata, TConstruction>
selection: Selection
metadata: TMetadata
} And private validate(parser: Parser<TContext>, validations: TSelectorValidation[]): void Infer type: FAILconst selectorValidations = this.selectors.map(selector => selector.validate(parser))
return this.validate(parser, selectorValidations) // TS2345
Constraint assignation to template alias: FAILconst selectorValidations: TSelectorValidation = this.selectors.map(selector => selector.validate(parser)) // TS2332
return this.validate(parser, selectorValidations) // TS2345
Cast to template alias: FAILconst selectorValidations = this.selectors.map(selector => selector.validate(parser)) as TSelectorValidation // TS2352
return this.validate(parser, selectorValidations) // TS2345
Use the complete and two light years long type: SUCCESSUsing the complete type in the prototype, TypeScript won't complain anymore, but my developer fingers, eyes and heart are burning. private validate(parser: Parser<TContext>, validations: TSelectorValidation<TContext, unknown, unknown>[]) const selectorValidations = this.selectors.map(selector => selector.validate(parser))
return this.validate(parser, selectorValidations) // Its ok now... This proves further the true need for this feature. Nothing I know is able to fully implement working type aliases, and my code is 60% template names, VS 40% useful typescript. What do you think about those cases @weswigham ? |
Well, I am now running into a wall with my own workaround, as I want to create new generic types that are depending on the class type. But since higher kinded types are not supported, that is impossible. As it stands, it's impossible for me to extract my generic class into a library without some sort of code generation. Bummer. |
There are a handful of issues around allowing type parameters to be omitted (such as #26242 and #16597). This seems related by going a step further and requiring it to be omitted. Perhaps the solutions could also be related...? Thinking of these as private generic template type parameters suggests an alternative syntax: class Foo<TProvidedByUser, private TDerived = Complex<Expression<On<TProvidedByUser>>>> {
// ...
} The type checker could reason about it well enough to know that the default is necessarily a lower bound - so no |
@shicks the I also think the double extends is redundant and can break existing code bases when attempting to introduce new generic type parameters to the class. I hope this issue gets the attention it deserves |
As currently implemented, that's correct. My point was that the This came up again recently in js-temporal/temporal-polyfill/pull/183, where the author was trying to initialize a few complex derived types and wrote (slightly abbreviated for conciseness) export function PrepareTemporalFields<
FieldKeys extends AnyTemporalKey,
OwnerT extends Owner<FieldKeys>,
RequiredFieldKeys extends FieldKeys,
RequiredFields extends readonly RequiredFieldKeys[] | FieldCompleteness,
ReturnT extends (RequiredFields extends 'partial' ? Partial<OwnerT> : FieldObjectFromOwners<OwnerT, FieldKeys>
)>(
bag: Partial<Record<FieldKeys, unknown>>,
fields: readonly FieldKeys[],
requiredFields: RequiredFields
): ReturnT {
// ... details elided ...
return result as unknown as ReturnT;
} In this case, the intended usage is that none of the parameters are explicitly provided. As written, though, declare const internal: unique symbol;
type Wrap<T> = {[internal](arg: T): T};
type Unwrap<W extends Wrap<any>> = W extends Wrap<infer T> ? T : never;
type EnsureWrapped<U extends Wrap<any>, T> = Wrap<any> extends U ? T : never;
declare function safer<
T,
TReturn extends Wrap<any> = Wrap<{foo: T}>
>(arg: EnsureWrapped<TReturn, T>): Unwrap<TReturn>; It would be nice not to have to do this, but instead just to declare |
Thanks for explaining Basically I had two generic type parameters that were somehow linked to each other and I had to verify. I did that by using operators in the constructor. Your code makes it a little more obvious that the default parameter is indeed the intended one. Cheers |
I consider this part of a handful of related issues needed for library-friendly type checking. I recently put together a "wish list" (see this gist) for a few features that I'd like to see in TypeScript generics, and that have some pretty solid synergy (such that any one by itself may not be particularly compelling, but when combined with the others, it makes some significant improvements). These are clearly relevant issues that a lot of people would like to see addressed. It would be great if we could get some more eyes on them, particularly from the TypeScript maintainers. |
This would enable the following pattern. Instead of: class A {
method(a) {
type Result = a extends ... infer ...
return result as Result;
}
} one could do: class A {
type Result<A> = A extends ... infer ...
method<A>(a: A): Result<A> {
return result;
}
} now imagine that type is used in multiple methods (yes, I know it can be pulled out of the class, but sometimes it is nicer to have those types next to the class methods where it is used): class A {
type Result<A> = A extends ... infer ...
method1<A>(a: A): Result<A> {
return result;
}
method2<A>(a: A): Result<A> {
return result;
}
method3<A>(a: A): Result<A> {
return result;
}
} |
Basically, instead of this: class A<Methods> {
public fn<K extends keyof Methods>(method: K) {
type Res = Methods[K] extends WorkerMethod<any, infer R> ? R : never;
type Chan = Methods[K] extends WorkerCh<any, infer I, infer O, any> ? [I, O] : never;
return this.module.fn<Res, Chan[0], Chan[1]>(method as string);
}
} I would like to be able to write: class A<Methods> {
public fn<K extends keyof Methods>(method: K) {
return this.module.fn<Res<K>, In<K>, Out<K>>(method as string);
}
type Res<K extends keyof Methods> = Methods[K] extends WorkerMethod<any, infer R> ? R : never;
type In<K extends keyof Methods> = Methods[K] extends WorkerCh<any, infer I, infer O, any> ? I : never;
type Out<K extends keyof Methods> = Methods[K] extends WorkerCh<any, infer I, infer O, any> ? O : never;
} |
It's hard to imagine why allowing type aliases in the scope of a class would be such an impactful change. It would enable a clearer, and more expressive separation of type-logic from runtime logic, bringing it inline with what's possible in functions and closures. I think this is in line with Typescript's design guideline:
|
+1 for more readable code with this feature |
I found a small "hack" to do this: class FooHelper<T extends { id: IndexableType }, Id = T['id']> {
public getById(id: Id): T {
return { id: 0 } as T;
}
}
export class Foo<T extends { id: IndexableType }> extends FooHelper<T> {} So basically you define the types as additional generic parameters of the class and use another class to ensure, that those generic parameters keep their default value. So inside Of course this is just a hacky workaround and I also want to be able to define type aliases in classes. P.S.: If it is safe enough for your use case you can just use this: export class Foo<T extends { id: IndexableType }, Id extends T['id'] = T['id']> {
public getById(id: Id): T {
return { id: 0 } as T;
}
} The user could now change the type of The first approach hides this type parameter completely from the user of the class. |
I normally want my class declaration to be the first thing in my file:
So when I want to declare an alias, I don't like the fact that my class has been pushed down further:
I suggest the following be permitted:
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 typefoobar
is private to the class. It should not be permissible to exportfoobar
.The text was updated successfully, but these errors were encountered: