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

Allow accessors to support inline generics for this #35986

Open
5 tasks done
arciisine opened this issue Jan 3, 2020 · 6 comments
Open
5 tasks done

Allow accessors to support inline generics for this #35986

arciisine opened this issue Jan 3, 2020 · 6 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@arciisine
Copy link

arciisine commented Jan 3, 2020

Search Terms

getter, accessor, this, generic

Suggestion

Currently accessor declarations do not support generics at the declaration site. The error returned is An accessor cannot have type parameters.ts(1094). Currently the accessor can implement generic declaration via the enclosing class. The goal would be to allow for the accessor to support a generic this at the declaration site.

Use Cases

The use case is to allow for more expressive setter/getter declarations, in line with how method declarations currently work.

Examples

Suggested Pattern

Below is an example of defining the constraints of this at the accessor site, to determine if an inherited accessor method is valid by the shape of the subclass.

class Base {
   get left<T extends { _left: string}>(this: T) {
       return this._left;
   } 
   get right<T extends { _right: string}>(this: T) {
       return this._right;
   } 
}

...
class Left extends Base {
    _left: string = '';
}
new Left().left; // OK
new Left().right;  // Errors

...
class Right extends Base {
   _right: string = '';
}
new Right().right; // OK
new Right().left; // Errors

...
class Both extends Base {
   _right: string = '';
   _left: string = '';
}
new Both().right; // OK
new Both().left; // OK

This pattern can currently be emulated by converting the accessors into standard methods

class Base {
  getLeft<T extends { _left: string}>(this: T) {
      return this._left;
  } 
  getRight<T extends { _right: string}>(this: T) {
      return this._right;
  } 
}

...
class Left extends Base {
   _left: string = '';
}
new Left().getLeft(); // OK
new Left().getRight(); // Errors

...
class Right extends Base {
  _right: string = '';
}
new Right().getRight(); // OK
new Right().getLeft(); // Errors

...
class Both extends Base {
  _right: string = '';
  _left: string = '';
}
new Both().getRight(); // OK
new Both().getLeft(); // OK

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Jan 7, 2020
@twbje
Copy link

twbje commented Jan 20, 2020

I would love that feature, too. I would like to use getters for "modifiers" without arguments, that return a modified object of the same type. E.g.:

class A {
  protected val: string;
  public withString<T extends A>(this: T, val: string): T {
    this.val = val;
    return this;
  }

  public get And<T extends A>(this: T): T {
    return this;
  }

  public get inUpperCase<T extends A>(this: T): T {
    this.val = this.val.toUpperCase();
    return this;
  }

  public toString(): string {
    return this.val;
  }
}

const a = new A().withString("test").And.InUpperCase;

console.out(a.toString()); // TEST

In this particular case, we could return type A, but as soon as we start with inheritance:

class B extends A {
}

const b = new B().toUpperCase;

the type of b would be converted to A.

@JohnLouderback
Copy link

I agree with the need for this. The use case I ran in to today required me to change my getter to a method, which is annoying as it's inconsistent with the rest of my code and I'm only doing it to support the type parameters.

  public getAllSettings<T extends this>(): Array<Setting<T>> {
    return (allPropertiesOf(this) as Array<keyof T>)
      .filter(prop => Reflect.getMetadata('setting', this, prop as string))
      .map(prop => {
        const metadata = Reflect.getMetadata('setting', this, prop as string);
        return {
          name: metadata.name,
          controlType: metadata.controlType,
          value: new Ref(this as T, prop)
        };
      });
  }

I need this because my method is defined in an abstract parent class. If I simply use keyof this it only allows for keys in the parent class and not the subclasses that inherent from it.

@jakub-gonet
Copy link

Another use-case for this is writing custom logic when exporting modules, e.g. memoizing things.
AFAIK the only possible way to do that currently is to use module.exports getters or Proxy.

Here's a simple example:

interface Item<T> {
    f: (value: T) => number;
}

declare function createObj<T>(): Item<T>

const MEMO: Record<string, Item<any>> = {}

module.exports = {
	// ⬇️ An accessor cannot have type parameters.(1094)⬇️ 
    get Container<T>(): Item<T> {
        if(!MEMO.Container) {
            MEMO.Container = createObj<T>();
        }
        return MEMO.Container;
    }
}

We stumbled upon this issue in RNGH after we rewrote the library to TS and tried to type the FlatList. This is the previous type declaration, this is an implementation of the FlatList wrapper. And here's the current type of FlatList for completeness.

@HuiiBuh
Copy link

HuiiBuh commented Feb 19, 2021

This sounds great and would allow something like this:

type Exact<T, SHAPE> = T extends SHAPE ? (Exclude<keyof T, keyof SHAPE> extends never ? T : never) : never;

set someData<T extends SliderImageSmall | SliderImage>(
  value: (T extends SliderImageSmall ? Exact<T, SliderImageSmall> : Exact<T, SliderImage>)[],
) {
  this.someData = value
}

@wycats
Copy link

wycats commented Feb 10, 2022

I would also love this. I hit this case when trying to use a mapped type to create static properties on a dynamic subclass.

Here's an example (playground link) for a simple enum-like abstraction:

declare class Simple<K extends string> {
    match<U>(matcher: { [P in K]: () => U }): U;
}

type SimpleConstructor<K extends string> = {
    new(key: K): Simple<K>;
}

type Class = abstract new(...args: any[]) => any;

type SimpleClass<K extends string> = SimpleConstructor<K> & {
    [P in K]: <This extends Class>(this: This) => InstanceType<This>;
}

declare function Enum<K extends string[]>(...keys: K): SimpleClass<K[number]>;

class Bool extends Enum("True", "False") {}

let truthy = Bool.True();
let falsy = Bool.False();

let truthyTruth = ifTrue(truthy, () => "truthy!");
let falsyTruth = ifTrue(falsy, () => "truthy!");

function ifTrue<U>(bool: Bool, callback: () => U): U | void {
    return bool.match({
        True: callback,
        False: () => undefined
    })
}

Because I am able to use a this generic in static methods, I can correctly instantiate the type. However, since I cannot use a this generic with static getters, I cannot instantiate the type if I use a getter.

Since getters conceptually do have a this value, I wonder the rationale for distinguishing these cases. Is it a relic from a time before the type system understood the difference between property and getter.

@DanielRosenwasser
Copy link
Member

Feels like a slightly related issue (especially based on that comment) is #22509 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

8 participants