microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.19k stars 12.38k forks source link

Permit type alias declarations inside a class #7061

Open NoelAbrahams opened 8 years ago

NoelAbrahams commented 8 years ago

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.

OliverUv commented 5 years ago

This would be incredibly useful for us here: https://github.com/mikolalysenko/mudb/blob/master/src/schema/array.ts

To be able to locally declare type Contained = ValueSchema['identity']; and type Container = Contained[] to get rid of the 19 instances of ValueSchema['identity'][].

Obiwarn commented 5 years ago

I have some big classes with functions with complex return objects that I want to refactor.

class BigClass {
    ...
    getReferenceInfo(word: string): { isInReferenceList:boolean, referenceLabels:string[] } {
    ...
    }
}

I want to do something like this to improve readability (declare it close to my function):

class Foo {
    ...
    type ReturnObject = { isInReferenceList:boolean, referenceLabels:string[] };
    getReferenceInfo(word: string):ReturnObject   {
      ...
      }
}

But right now Typescript only lets me declare interfaces/types outside of the class:

type ReturnObject = { isInReferenceList:boolean, referenceLabels:string[] };
class Foo {
    ...
    getReferenceInfo(word: string):ReturnObject   {
      ...
      }
}

How would you handle that?

simeyla commented 5 years ago

@Obiwarn you can currently do the following

class Foo {
    ...
    getReferenceInfo(word: string)   {
      ...
      }
}

export type ReturnObject = ReturnType<Foo['getReferenceInfo']>;

This can't be done inside the class, but can be declared outside.

You cannot currently do the following - but I'm hoping someday something equivalent will be possible!

class Foo {
    ...
    getReferenceInfo(word: string) : infer ReturnType  {
      ...
      }
}
thejohnfreeman commented 5 years ago

Sans any context, I present a real example from production code:

  export function group<G extends ViewModelConstructorGroup>(
    ctors: G,
  ): GroupViewModelConstructor<ViewModelGroupIsomorphicTo<G>> {
    return {
      construct(
        initValues:
          | GroupViewModel<ViewModelGroupIsomorphicTo<G>>
          | Partial<ValueGroup<ViewModelGroupIsomorphicTo<G>>> = {},
      ): GroupViewModel<ViewModelGroupIsomorphicTo<G>> {
        return initValues instanceof GroupViewModel
          ? initValues
          : new GroupViewModel(map(ctors, (ctor, key) =>
              ctor.construct(initValues[key]),
            ) as ViewModelGroupIsomorphicTo<G>)
      },
    }
  }

That's 5 mentions of ViewModelGroupIsomorphicTo<G> that I would love an alias for.

rraval commented 5 years ago

@thejohnfreeman You can use type to declare an alias in a function body to reduce the 5 mentions to 2:

  export function group<G extends ViewModelConstructorGroup>(
    ctors: G,
  ): GroupViewModelConstructor<ViewModelGroupIsomorphicTo<G>> {
    type Iso = ViewModelGroupIsomorphicTo<G>;
    return {
      construct(
        initValues:
          | GroupViewModel<Iso>
          | Partial<ValueGroup<Iso>> = {},
      ): GroupViewModel<Iso> {
        return initValues instanceof GroupViewModel
          ? initValues
          : new GroupViewModel(map(ctors, (ctor, key) =>
              ctor.construct(initValues[key]),
            ) as Iso)
      },
    }
  }

However, this still means that:

PreventRage commented 4 years ago

Here's my current chunk of code:

export interface EnumerateObjectProperties_Include {
    strings?:boolean;
    symbols?:boolean;
    enumerable?:boolean;
    nonEnumerable?:boolean;
    own?:boolean;
    inherited?:boolean;
    covered?:boolean;
}
export const EnumerateObjectProperties_Include_Standard:EnumerateObjectProperties_Include = {
    strings: true,
    enumerable: true,
    own: true,
};
export const EnumerateObjectProperties_Include_AllOwn:EnumerateObjectProperties_Include = {
    strings: true,
    symbols: true,
    enumerable: true,
    nonEnumerable: true,
    own: true,
};
export type ObjectPropertyDescriptor = ObjectKeyDescriptor & PropertyDescriptor;
export function * EnumerateObjectProperties(
    obj:object|undefined,
    include:EnumerateObjectProperties_Include,
):Generator<ObjectPropertyDescriptor> {
    if (!include.enumerable && !include.nonEnumerable) {
        return;
    }
    let opd:ObjectPropertyDescriptor;
    for (opd of EnumerateObjectKeys(obj, include)) {
        const pd = Object.getOwnPropertyDescriptor(opd.obj, opd.key)!;
        if (!(pd.enumerable ? include.enumerable : include.nonEnumerable)) {
            continue;
        }
        Object.assign(opd, pd);
        yield opd;
    }
}

I so wish that it could be something more like the following. Which turns something like a dozen instances of "EnumerateObjectProperties" into one.

export <keyword> EnumerateObjectProperties {

    public interface Include {
        strings?:boolean;
        symbols?:boolean;
        enumerable?:boolean;
        nonEnumerable?:boolean;
        own?:boolean;
        inherited?:boolean;
        covered?:boolean;
    }

    public const Include.Standard = {
        strings: true,
        enumerable: true,
        own: true,
    };

    public const Include.AllOwn = {
        strings: true,
        symbols: true,
        enumerable: true,
        nonEnumerable: true,
        own: true,
    };

    export type Descriptor = EnumerateObjectKeys.Descriptor & PropertyDescriptor;
    export function * Enumerate(
        obj:object|undefined,
        include:Include,
    ):Generator<Descriptor> {
        if (!include.enumerable && !include.nonEnumerable) {
            return;
        }
        let d:Descriptor;
        for (d of EnumerateObjectKeys.Enumerate(obj, include)) {
            const pd = Object.getOwnPropertyDescriptor(d.obj, d.key)!;
            if (!(pd.enumerable ? include.enumerable : include.nonEnumerable)) {
                continue;
            }
            Object.assign(d, pd);
            yield d;
        }
    }
}

I find myself constantly creating "namespaces"/structure in my type names using _ separators. In this example they're actually two deep! EnumerateObjectKeys_Include_Standard.

I don't know what should be. In some languages it'd be class. If namespace weren't global it could be namespace. If modules could nest it could be module.

weswigham commented 4 years ago

You can actually use TS namespaces within a module; they won't be global (so long as the file has a top-level import or export), and do pretty much exactly what you describe.

PreventRage commented 4 years ago

How embarrassing. I've been allowing myself to be scared off by an eslint warning:

"ES2015 module syntax is preferred over custom TypeScript modules and namespaces. eslint(@typescript-eslint/no-namespace)

weswigham commented 4 years ago

Yeah, because you can create the exact same namespace structure by making a second file with all your intended nested namespace stuff, and then reexporting that whole file's namespace as some name in the first file. That construction is generally preferred, and is much more javascripty.

matt-curtis commented 4 years ago

The namespace approach for extending classes works, sort of. Is there any way to avoid having to give the fully qualified name of augmented member, or at least some kind of Self construct I can replace Car with?

class Car {
    wheelSize = Car.WheelSize.large;
}

namespace Car {
    export enum WheelSize { small, large, huge };
}
SrBrahma commented 3 years ago

If using the type keyword would be a problem in this case, a previous idea of mine could fit here too:

https://github.com/microsoft/TypeScript/issues/40780

Dimitri-WEI-Lingfeng commented 3 years ago

This would be very convinient in below scenario:


class Foo<T> {
  type TList = T[] // declare a local alias, so I don't need to duplicate T[] all the time.
}
simeyla commented 3 years ago

What I would LOVE is something like this:

class ProductPageComponent
{
   model(): export type ProductPageModel {
      return {
         products: ['pen', 'pencil', 'paper'],
         saleItems: ['pencil']
      };
   }
}

This would define ProductPageModel in place so it could be used elsewhere. I don't have to update a separate type declaration everytime I add something.

Dimitri-WEI-Lingfeng commented 3 years ago

This would be very convinient in below scenario:

class Foo<T> {
  type TList = T[] // declare a local alias, so I don't need to duplicate T[] all the time.
}

I've found that local type declaration is currently valid inside function block:

function Foo<T>(){
  type TList = T[]
}

But the inside type is not available in the signature:

function Foo<T>(): TList {  //  error
  type TList = T[]
}

So, i recommend a new syntax just like:

function Foo<T> with {
  // only types related syntax allowed here
  type TList = T[]
  type TTuple = [T, T]
  interface TDict {
    [k: string]: T
  }
  // ...
}(): TList { 
}

// the same for class
class Foo<T> with {
  type TList = T[]
} extends Parent<TList> implements InterfaceFoo<TList> { 
}
rgpublic commented 3 years ago

I'm soooo much missing this. I'm coming back to this issue here time and gain. Please, please dear beloved Microsoft gods could you finally implement this? With a small virtual lamb sacrifice perhaps? It would be immensely useful - especially for private/protected stuff inside a class with no connection to the "outside world". You could write stuff that much more efficiently. Currently I don't use custom types that much because I'm always afraid of name collisions. What's more, I'm using exactly 1 file per class at the moment. The file is named like that class name with ".ts". The file starts with "class .... {" and ends with "}". So the class is encapsulating the whole file. Very ugly now to have stuff floating around outside that class braces that's only ever used privately for the class itself.

Right10 commented 3 years ago

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)
Kyasaki commented 3 years ago

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.

Kyasaki commented 3 years ago

Actually, the following are different versions of the same code, implementing the previous interface; see by yourself.

Plain, painfull implementation

export 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 assignation

This is what leverages a bit the pain today, but please note the constraint repetitions in the form TAlias extends Pain<A, B, C> = Pain<A, B, C>.

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 list

This 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 aliases

It would still be interesting to have the capability to export some of those aliases with something like public type TAlias = Pain<A, B, C>.

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. Therefore with exported aliases we could have something like this:

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 modules

The 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.

kristiandupont commented 3 years ago

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 SomeDerivedType is what I would ideally like to declare using type inside the class. I do it here in the generic parameters instead. However, since I don't want it exposed in my API, I create a new class that only takes one generic parameter and relies on the defaultness of the second.

Kyasaki commented 3 years ago

Rationale

After 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:

Context

Be SelectorValidation a discriminated union type defined as so:

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 validate a method defined as so:

private validate(parser: Parser<TContext>, validations: TSelectorValidation[]): void

Infer type: FAIL

const selectorValidations = this.selectors.map(selector => selector.validate(parser))
return this.validate(parser, selectorValidations) // TS2345

Argument of type 'SelectorValidation<TContext, unknown, unknown>[]' is not assignable to parameter of type 'TSelectorValidation[]'. Type 'SelectorValidation<TContext, unknown, unknown>' is not assignable to type 'TSelectorValidation'. 'SelectorValidation<TContext, unknown, unknown>' is assignable to the constraint of type 'TSelectorValidation', but 'TSelectorValidation' could be instantiated with a different subtype of constraint 'SelectorValidation<TContext, unknown, unknown>'. Type 'InvalidSelectorValidation' is not assignable to type 'TSelectorValidation'. 'InvalidSelectorValidation' is assignable to the constraint of type 'TSelectorValidation', but 'TSelectorValidation' could be instantiated with a different subtype of constraint 'SelectorValidation<TContext, unknown, unknown>'.ts(2345)

Constraint assignation to template alias: FAIL

const selectorValidations: TSelectorValidation = this.selectors.map(selector => selector.validate(parser)) // TS2332
return this.validate(parser, selectorValidations) // TS2345

Type 'SelectorValidation<TContext, unknown, unknown>[]' is not assignable to type 'TSelectorValidation'. 'TSelectorValidation' could be instantiated with an arbitrary type which could be unrelated to 'SelectorValidation<TContext, unknown, unknown>[]'.ts(2322)

Cast to template alias: FAIL

const selectorValidations = this.selectors.map(selector => selector.validate(parser)) as TSelectorValidation // TS2352
return this.validate(parser, selectorValidations) // TS2345

Conversion of type 'SelectorValidation<TContext, unknown, unknown>[]' to type 'TSelectorValidation' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. 'TSelectorValidation' could be instantiated with an arbitrary type which could be unrelated to 'SelectorValidation<TContext, unknown, unknown>[]'.ts(2352)

Use the complete and two light years long type: SUCCESS

Using 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 ?

kristiandupont commented 2 years ago

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.

shicks commented 2 years ago

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 extends ... clause should be necessary.

nopeless commented 1 year ago

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 extends ... clause should be necessary.

@shicks the = is not the lower bound, but the default type supplied when there is no way to infer the data type or the user did not supply via arguments in <...>. So, extends keyword is necesary. TSC will complain about "can be instantiated with a different type unrelated to..." if you try the solution you posted.

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

shicks commented 1 year ago

@shicks the = is not the lower bound, but the default type supplied when there is no way to infer the data type or the user did not supply via arguments in <...>. So, extends keyword is necesary. TSC will complain about "can be instantiated with a different type unrelated to..." if you try the solution you posted.

As currently implemented, that's correct. My point was that the private specifier could tell the type checker that the user was not allowed to supply a different type in <...>, so it should be treated as an exact type. Since writing that I've learned that inference does still sometimes happen on types with a = (which I hadn't seen before, though it's still not as consistent as one might hope, else #26242 wouldn't be an issue), so maybe the syntax isn't quite correct here.

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, ReturnT is inferred rather than a simple direct alias. This means that if you assign the result of this function to a variable with any type that's a subtype of the actual return type (i.e. CorrectReturnType & SomeRandomInterface) then the type checker will happily back-infer this type and not complain, despite the fact that there's no way the function's implementation can possibly know about your interface to satisfy it (we call these "return-only generics" at Google and have banned them because they're inherently unsafe). Using an initializer doesn't help here, either, because it will still back-infer. The only solution I found that was completely safe against this issue was to wrap the type in the <...> and the unwrap it in the return, thus preventing the back-inference (playground):

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 TReturn as an internal alias.

nopeless commented 1 year ago

@shicks the = is not the lower bound, but the default type supplied when there is no way to infer the data type or the user did not supply via arguments in <...>. So, extends keyword is necesary. TSC will complain about "can be instantiated with a different type unrelated to..." if you try the solution you posted.

As currently implemented, that's correct. My point was that the private specifier could tell the type checker that the user was not allowed to supply a different type in <...>, so it should be treated as an exact type. Since writing that I've learned that inference does still sometimes happen on types with a = (which I hadn't seen before, though it's still not as consistent as one might hope, else #26242 wouldn't be an issue), so maybe the syntax isn't quite correct here.

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, ReturnT is inferred rather than a simple direct alias. This means that if you assign the result of this function to a variable with any type that's a subtype of the actual return type (i.e. CorrectReturnType & SomeRandomInterface) then the type checker will happily back-infer this type and not complain, despite the fact that there's no way the function's implementation can possibly know about your interface to satisfy it (we call these "return-only generics" at Google and have banned them because they're inherently unsafe). Using an initializer doesn't help here, either, because it will still back-infer. The only solution I found that was completely safe against this issue was to wrap the type in the <...> and the unwrap it in the return, thus preventing the back-inference (playground):

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 TReturn as an internal alias.

Thanks for explaining Sorry for misunderstanding what you originally said. I played around with the unwrapped type inference you had and its a little similar to what I had

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

shicks commented 1 year ago

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.

streamich commented 1 year ago

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;
  }
}
streamich commented 1 year ago

Here is an excerpt from real code:

image

Note, the types depend on Methods class generic, so the types cannot be easily extracted outside of the class, that Methods would need to be passed as a param.

streamich commented 1 year ago

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;
}

image
martaver commented 11 months ago

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. Produce a language that is composable and easy to reason about.
ibraheemhlaiyil commented 2 months ago

+1 for more readable code with this feature

Pyrdacor commented 1 month ago

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 FooHelper you can add all the implementation and can use the type Id for more readability. But you only export Foo so the user can't change the type through type arguments.

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 Id but it is ensured that it at least extends T['id'].

The first approach hides this type parameter completely from the user of the class.