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

Figure out the shape of component type parameters #25

Closed
dfreeman opened this issue Nov 24, 2020 · 4 comments · Fixed by #58
Closed

Figure out the shape of component type parameters #25

dfreeman opened this issue Nov 24, 2020 · 4 comments · Fixed by #58
Labels
design Figuring out how things should work

Comments

@dfreeman
Copy link
Member

Currently, @glimmer/component looks something like this:

// @glimmer/component
class Component<Args> {
  readonly args: Args;
}

// my-component.ts
interface MyComponentArgs {
  foo: string;
}

class MyComponent extends Component<MyComponentArgs> {}

This RFC suggests adding a second type parameter to capture the types of a component's yields, something like:

// @glimmer/component
class Component<Args, Yields> {
  readonly args: Args;

  // Not visible anywhere from TS, but necessary to capture the type
  [YieldsBrand]: Yields;
}

// my-component.ts
interface MyComponentArgs {
  foo: string;
}

interface MyComponentYields {
  default: [message: string];
}

class MyComponent extends Component<MyComponentArgs, MyComponentYields> {}

However, this may not scale well as we consider other information we could eventually want to encode, such as #21. Capturing the type of a template's root element would add a third type param, and if we decide to allow authors to explicitly specify valid attrs, that would be a fourth.

A class with four type parameters is already unwieldy, but it's especially so if not every parameter will always be specified. An author might never yield from their component, but still have ...attributes applied to an element.

Given that, a signature along these lines may be more ergonomic (and more amenable to future expansion as necessary):

interface ComponentSignature {
  args?: object;
  blocks?: object;
  element?: Element;
}

class Component<T extends ComponentSignature> {
  readonly args: T['args'];

  // As above, not visible from TS but necessary to capture the full type
  [SignatureBrand]: T;
}

However, there are some questions we need to answer:

  • Is the danger of typos an issue? ({ arrgs: MyComponentArgs } would be valid, just not useful)
  • Adding type parameters is backwards compatible (as long as there's a default), but nesting component args under an args key in the type would be a breaking change for @glimmer/component—how much trouble would that cause?
@dfreeman dfreeman added the design Figuring out how things should work label Nov 24, 2020
@dfreeman
Copy link
Member Author

A related issue to this is the fact that it's currently not possible to write a typesafe class-based helper in a way that the typechecker deems valid.

import Helper from '@ember/component/helper';

export default class ShoutHelper extends Helper<[string], {}, string> {
  public compute([message]: [string]): string {
    return message.toUpperCase();
  }
}

Because compute is declared in the base class to accept (params: any[], hash: object), requiring a tuple of a specific length is an invalid override, since it narrows the accepted type of a parameter.

This is similar to the discrepancy we face with trying to rework Component<Args> to Component<{ args: Args, ... }>, since it conflicts with the existing base type declaration. Ultimately, the base types can hopefully be shifted to something that plays nicely, but while glint is still in an experimental phase, we need to find an alternative approach.

One option may be to re-export values like Component and Helper from their source packages, but declared to have more amenable types.

@dfreeman
Copy link
Member Author

dfreeman commented Feb 17, 2021

@jamescdavis @chriskrycho this issue is a partial capture of the "Glimmer 2 type params" question we have as an action item

@chriskrycho
Copy link
Member

Quick question: AFAICT from reading and thinking about this, even fixing the type params (and doing as enabled by #54) wouldn't help with the variance problem for the type of positional params, would it?

@dfreeman
Copy link
Member Author

dfreeman commented Mar 7, 2021

Weirdly I think the problem is actually with the named: object bit: changing that to named: {} eliminates the complaint from the typechecker (though just by observation you can see that it's still incorrect).

Really the ideal would be to incorporate the type params in the base class, because then the variance works out correctly based on their position of usage. For now, though, what I'm looking at doing is essentially this:

interface Helper<T> extends Omit<EmberHelper, 'compute'> {
  compute(positional: ..., named: ...): ...;
}

This means an instance of this Helper type would never be assignable to the upstream Helper type, but in practice I think that's fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
design Figuring out how things should work
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants