Skip to content

Support helper-based type narrowing #10

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

Merged
merged 2 commits into from
Aug 6, 2020
Merged

Conversation

dfreeman
Copy link
Member

@dfreeman dfreeman commented Aug 6, 2020

The level of complexity in helper signatures currently prohibits them from acting as type guards, which is especially problematic since templates don't have a native notion of equality, so you can't do the equivalent of ad-hoc if (obj.discriminatorField === 'discriminant-value') checks.

This PR makes a few changes that, together, enable helpers to narrow the types of their arguments.

// Today, a helper signature looks something like this:
declare const isNumber: (named: NoNamedArgs, value: unknown) => ReturnsValue<boolean>;

// And invoking it in a conditional would translate to code like this:
if (invokeInline(resolve(isNumber)({}, maybeANumber))) {
  // ...unfortunately, we know nothing about `maybeANumber` here
}

There are two things that prevent isNumber from narrowing the type of maybeANumber. First, the ReturnsValue makes it impossible to represent a value is number at all, as such is clauses are restricted syntactically so that they can only be the direct return type of a function. Second, even with that solved, the wrapping invokeInline function would still degrade a type guard to a regular boolean-returning function.

The change in this PR eliminates the ReturnsValue macro, so that helpers are represented essentially as their raw function types. Pulling on the thread of that change, signatures as a whole can become much simpler, with the CreatesModifier type becoming a plain marker object. The Invokable<T> wrapper and AnySignature type both go away entirely, as they'd already been weakened nearly to the point of doing nothing and with this change they no longer deliver any value at all.

Secondly, it eliminates invokeInline as a peer of invokeModifier and invokeBlock. Instead, invokeEmit now exists, but is only applied to values that will be emitted into the DOM either directly as content or into an attribute or concatenated string (which provides a bit of type safety against [object Object] showing up on screen). In cases where the return value of a helper is actually treated as data, such as a component @arg or a helper parameter, no invoke helper is required at all and the value is passed directly.

Together, these changes make the snippet above into something more like:

declare const isNumber: (named: NoNamedArgs, value: unknown) => value is number;

if (resolve(isNumber)({}, maybeANumber)) {
  // here, we know that `maybeANumber` is in fact a number
}

@dfreeman dfreeman added the enhancement New feature or request label Aug 6, 2020
@dfreeman dfreeman merged commit e4135ba into master Aug 6, 2020
@delete-merged-branch delete-merged-branch bot deleted the conditional-narrowing branch August 6, 2020 20:23
@chriskrycho
Copy link
Member

Ooooh, I like this change a lot. 👏

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.

2 participants