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

Support Signature types for modifiers #210

Merged
merged 1 commit into from
Mar 21, 2022
Merged

Support Signature types for modifiers #210

merged 1 commit into from
Mar 21, 2022

Conversation

chriskrycho
Copy link
Contributor

@chriskrycho chriskrycho commented Mar 19, 2022

Details

Following the design pioneered in emberjs/rfcs#748, introduce a new Signature type and use it in defining the types for both function- and class-based modifiers. This allows end users to write, for example:

interface PlaySig {
  Args: {
    Named: {
      when: boolean;
    }
  }
  Element: HTMLMediaElement;
}

export default modifier<PlaySig>((el, _, { when: shouldPlay }) => {
  if (shouldPlay) {
    el.play();
  } else {
    el.pause();
  }
});

This supports both Named and Positional args, and works equally well with class-based modifiers. It also provides the Element type as Element by default, and provides internal type utilities as well as one external-facing type utility to make writing the type of args when passed to a modifier constructor easy to do in terms of the Signature which represents the modifier.

Users do not need to type this form: the form using basic inference continues to work as well:

export default modifier(
  (
    el: HTMLMediaElement,
    _: [],
    { when: shouldPlay }: { when: boolean }
  ) => {
    if (shouldPlay) {
      el.play();
    } else {
      el.pause();
    }
  })
);

That is: the support for Signatures is strictly new capability which was not present before.

The same kind of signature works with class-based modifiers as well, while retaining backward compatibility with the Args-based signature and declaration on the subclass element field directly. To support that in a way that does not require users to name their args over and over again, introduce an ArgsFor type utility which translates from the signature type to the runtime form:

import Modifier, { ArgsFor } from 'ember-modifier';

interface PlaySig {
  Args: {
    Named: {
      when: boolean;
    };
  };
  Element: HTMLMediaElement;
}

class MyModifier extends Modifier<PlaySig> {
  constructor(owner: unknown, args: ArgsFor<PlaySig>) {
    super(owner, args);
    // ...
  }

  didReceiveArguments() {
    const shouldPlay = this.args.named.when;
    if (shouldPlay) {
      this.element.play();
    } else {
      this.element.pause();
    }
  }
}

Additionally, the modifier function now returns an opaque FunctionBasedModifier type. This mostly exists to provide nice hooks for tooling to hook onto, but it has the added benefit of showing something besides unknown in an editor, and that "something" shows the arguments and element specified for the modifier.

Supporting changes

  • Update the README to match, and fix a lot of long-since outdated documentation along the way.

  • Loosen type test constraint to support older TS versions

    The new type signature is compatible (as the tests on TS 4.5+ show), but on older versions of TS, it does not check under a type equality check, only under a type matching check. For the purposes of the specific test in question, that's perfectly fine, because the other type tests cover inference and resolution correctly.

@chriskrycho chriskrycho mentioned this pull request Mar 19, 2022
19 tasks
@chriskrycho chriskrycho marked this pull request as ready for review March 19, 2022 22:32
Details
-------

Following the design pioneered in emberjs/rfcs#748, introduce a new
`Signature` type and use it in defining the types for both function-
and class-based modifiers. This allows end users to write, for example:

    interface PlaySig {
      Args: {
        Named: {
          when: boolean;
        }
      }
      Element: HTMLMediaElement;
    }

    export default modifier<PlaySig>((el, _, { when: shouldPlay }) => {
      if (shouldPlay) {
        el.play();
      } else {
        el.pause();
      }
    });

This supports both `Named` and `Positional` args, and works equally
well with class-based modifiers. It also provides the `Element` type as
`Element` by default, and provides internal type utilities as well as
one external-facing type utility to make writing the type of `args`
when passed to a modifier `constructor` easy to do in terms of the
`Signature` which represents the modifier.

Users do not *need* to type this form: the form using basic inference
continues to work as well:

    export default modifier((
      el: HTMLMediaElement,
      _: [],
      { when: shouldPlay }: { when: boolean }
    ) => {
      if (shouldPlay) {
        el.play();
      } else {
        el.pause();
      }
    }));

That is: the support for `Signatures` is strictly *new* capability
which was not present before.

The same kind of signature works with class-based modifiers as well,
while retaining backward compatibility with the `Args`-based signature
and declaration on the subclass `element` field directly. To support
that in a way that does not require users to name their args over and
over again, introduce an `ArgsFor` type utility which translates from the signature type to the runtime form:

    import Modifier, { ArgsFor } from 'ember-modifier';

    interface PlaySig {
      Args: {
        Named: {
          when: boolean;
        };
      };
      Element: HTMLMediaElement;
    }

    class MyModifier extends Modifier<PlaySig> {
      constructor(owner: unknown, args: ArgsFor<PlaySig>) {
        super(owner, args);
        // ...
      }

      didReceiveArguments() {
        const shouldPlay = this.args.named.when;
        if (shouldPlay) {
          this.element.play();
        } else {
          this.element.pause();
        }
      }
    }

Additionally, the `modifier` function now returns an opaque
`FunctionBasedModifier` type. This mostly exists to provide nice hooks
for tooling to hook onto, but it has the added benefit of showing
something besides `unknown` in an editor, and that "something" shows
the arguments and element specified for the modifier.

Supporting changes
------------------

-   Update the README to match, and fix a *lot* of long-since outdated
    documentation along the way.

-   Loosen type test constraint to support older TS versions

    The new type signature *is* compatible (as the tests on TS 4.5+
    show), but on older versions of TS, it does not check under a type
    equality check, only under a type matching check. For the purposes
    of the specific test in question, that's perfectly fine, because
    the *other* type tests cover inference and resolution correctly.
@chriskrycho chriskrycho merged commit a8ead80 into master Mar 21, 2022
@chriskrycho chriskrycho deleted the types branch March 21, 2022 16:55
@chriskrycho chriskrycho added the enhancement New feature or request label Mar 21, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant