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

[BUGFIX] improve component argument types for TS-JS interop #286

Closed

Conversation

chriskrycho
Copy link
Contributor

@chriskrycho chriskrycho commented Apr 24, 2020

Component currently types its arguments as extending {}:

class Component<Args extends {} = {}> {
  readonly args: Args;
  constructor(owner: unknown, args: Args);
}

This works in pure-JS or pure-TS codebases, but breaks down in mixed codebases, especially where users are using the @ts-check pragma or where we want to feed information from component definitions into the TS language server to get better completion info throughout an app.

For example, given this component in a JS codebase with @ts-check enabled:

class Profile extends Component {
  get description() {
    return `${this.args.name} is ${this.args.age} years old!`;
  }
}

The TS language server (e.g. in an editor) will report:

Property 'name' does not exist on type '{}'.
Property 'age' does not exist on type '{}'.

To eliminate this problem while maintaining compatibility with all existing typed usages (including some we might like to forbid but cannot at present, like passing Component<[]> -- thanks JavaScript!), we switch from {} to Record<string, any>:

class Component<Args extends Record<string, any> = Record<string, any>> {
  readonly args: Args;
  constructor(owner: unknown, args: Args);
}

Now, the error described above is eliminated for JS consumers using TS information in any way, but TS users can continue to supply correct types for their components.


A note on Record, which was added in TS 2.1 (and see also the note on the handbook): using it safely requires explicitly calling out optional or nullable types. For example:

let a: Record<string, string> = {};
let b: number = a['anything']; // type checks, but...
let c = b.length; // RUNTIME ERROR 😱

It's our old friend:

TypeError: undefined is not an object (evaluating 'b.length')

Accordingly, most uses of Record should be of the form Record<string, T | undefined>, and a safe Dict<T> would be exactly that:

type Dict<T> = Record<string, T | undefined>;

`Component` currently types its arguments as extending `{}`:

    class Component<Args extends {} = {}> {
      readonly args: Args;
      constructor(owner: unknown, args: Args);
    }

This works in pure-JS or pure-TS codebases, but breaks down in mixed
codebases, especially where users are using the `@ts-check` pragma or
where we want to feed information from component definitions into the TS
language server to get better completion info throughout an app.

For example, given this component in a JS codebase with `@ts-check`
enabled:

    class Profile extends Component {
      get description() {
        return `${this.args.name} is ${this.args.age} years old!`;
      }
    }

The TS language server (e.g. in an editor) will report:

> Property 'name' does not exist on type '{}'.
> Property 'age' does not exist on type '{}'.

To eliminate this problem while maintaining compatibility with all
existing typed usages (including some we might like to forbid but cannot
at present, like passing `Component<[]>` -- thanks JavaScript!), we
switch from `{}` to `Record<string, any>`:

    class Component<Args extends Record<string, any> = Record<string, any>> {
      readonly args: Args;
      constructor(owner: unknown, args: Args);
    }

Now, the error described above is eliminated for JS consumers using TS
information in any way, but TS users can continue to supply correct
types for their components.
@chriskrycho
Copy link
Contributor Author

(Just force pushed to fix a typo in the commit message. Can't have that hanging around for all of time. 😂)

@rwjblue
Copy link
Member

rwjblue commented Apr 27, 2020

Thanks for working on this! Can you add a (previously failing) test to test/types/component-test.ts (which is ran through dtslint) that is now fixed? Specifically, if this usage pattern is expected (and supported) I want to ensure that we don't accidentally loose those constraints in any future work.

@rwjblue rwjblue marked this pull request as draft April 27, 2020 13:51
@chriskrycho
Copy link
Contributor Author

Yes, will do – have it up in an hour or so!

@chriskrycho
Copy link
Contributor Author

Superseded by emberjs/rfcs#748.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants