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 type arguments in generic tagged templates #23430

Merged
merged 20 commits into from
Apr 19, 2018

Conversation

DanielRosenwasser
Copy link
Member

@DanielRosenwasser DanielRosenwasser commented Apr 16, 2018

This pull request allows users to pass generic type arguments to tagged template strings.

declare function styledComponent<Props>(strs: TemplateStringsArray): Component<Props>;

interface MyProps {
  name: string;
  age: number;
}

styledComponent<MyProps> `
  font-size: 1.5em;
  text-align: center;
  color: palevioletred;
`;

declare function tag<T>(strs: TemplateStringsArray, ...args: T[]): T;

// inference fails because 'number' and 'string' are both candidates that conflict
let a = tag<string | number> `${100} ${"hello"}`;

Fixes #11947

Background

Tagged templates are a form of invocation introduced in ECMAScript 2015. Like call expressions, generic functions may be used in a tagged template and TypeScript will infer the type arguments utilized:

declare function tag<T>(strs: TemplateStringsArray, ...args: T[]): T;

let a = tag(100, 200);     // has type 'number'
let b = tag `Hello world`; // has type TemplateStringsArray

However, in some cases there are no inference candidates

declare function styledComponent<Props>(strs: TemplateStringsArray): Component<Props>

// has type 'Component<{}>' because there were no candidates
let a = styledComponent `
  font-size: 1.5em;
  text-align: center;
  color: palevioletred;
`;

or, type arguments cannot be inferred because TypeScript is conservative in its inferences

declare function tag<T>(strs: TemplateStringsArray, ...args: T[]): T;

// inference fails because 'number' and 'string' are both candidates that conflict
let b = tag `${100} ${"hello"}`;

Parser changes

The core change is a new intermediate grammar production between MemberExpression and CallExpression/NewExpression

TaggedTemplateWithTypeArguments:
    TaggedTemplateWithTypeArguments < TypeArgumentList > TemplateLiteral
    MemberExpression

Then CoverCallExpressionAndAsyncArrowHead (which is the grammar production that currently transitions a CallExpression into a MemberExpression) no longer references MemberExpression Arguments, but instead looks more like:

CoverCallExpressionAndAsyncArrowHead:
    TaggedTemplateWithTypeArguments Arguments

Similarly, NewExpression no longer references MemberExpression, and instead goes for

NewExpression:
    TaggedTemplateWithTypeArguments TypeArgumentsArguments
    new TaggedTemplateWithTypeArguments Arguments

Within our mechanics for MemberExpression still use the following production for tagged template expressions (which you can see in parseMemberExpressionRest)

MemberExpression:
    MemberExpression TemplateLiteral

This way we always try to parse out a template after a MemberExpression, even when there are no type arguments.

@lucasterra
Copy link

Beautiful! Thank you :)

@Havret
Copy link

Havret commented Apr 16, 2018

Thank you so much! It really made my day. :)

@MartinJohns
Copy link
Contributor

Can you please also update the spec according to the new changes?

@@ -3516,8 +3517,8 @@ declare namespace ts {
function updateCall(node: CallExpression, expression: Expression, typeArguments: ReadonlyArray<TypeNode> | undefined, argumentsArray: ReadonlyArray<Expression>): CallExpression;
function createNew(expression: Expression, typeArguments: ReadonlyArray<TypeNode> | undefined, argumentsArray: ReadonlyArray<Expression> | undefined): NewExpression;
function updateNew(node: NewExpression, expression: Expression, typeArguments: ReadonlyArray<TypeNode> | undefined, argumentsArray: ReadonlyArray<Expression> | undefined): NewExpression;
function createTaggedTemplate(tag: Expression, template: TemplateLiteral): TaggedTemplateExpression;
function updateTaggedTemplate(node: TaggedTemplateExpression, tag: Expression, template: TemplateLiteral): TaggedTemplateExpression;
function createTaggedTemplate(tag: Expression, typeArguments: NodeArray<TypeNode>, template: TemplateLiteral): TaggedTemplateExpression;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is an API breaking change. we need to document it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the other option is to put it at the end, or have multiple overloads. check with @rbuckton

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I wanted to get the conversation rolling on this one. Every time we change the AST, these factory functions get pretty annoying to update. I wonder whether VMs have gotten good at optimizing the "named arguments" pattern for options-bag style APIs.

@@ -1032,17 +1032,19 @@ namespace ts {
: node;
}

export function createTaggedTemplate(tag: Expression, template: TemplateLiteral) {
export function createTaggedTemplate(tag: Expression, typeArguments: NodeArray<TypeNode>, template: TemplateLiteral) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the API would be easier to use if the parameter is typed as ReadonlyArray and it's converted to a NodeArray in the function body.
this also applies to the update function below.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call!

@DanielRosenwasser
Copy link
Member Author

DanielRosenwasser commented Apr 17, 2018

@mhegazy, the API is purely additive now, minimizing breaking changes.
@ajafff, the API now converts ReadonlyArrays to NodeArrays.

@@ -17749,7 +17749,11 @@ namespace ts {

let typeArguments: NodeArray<TypeNode>;

if (!isTaggedTemplate && !isDecorator && !isJsxOpeningOrSelfClosingElement) {
if (isTaggedTemplate) {
Copy link
Member

@weswigham weswigham Apr 19, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure this duplicates the logic in the else if below, no? You should just need to add to the CallExpression cast below. (I'd add a CallLikeExpressionWithTypeArguments, that's CallLikeExpression sans decorators, which are the only one without them)

@MartinJohns
Copy link
Contributor

No update of the spec. :-(

@sergeysova
Copy link

What about passed functions?

const Example = styled.div`
  font-size: ${p => p.size}rem;
  flex-direction: ${p => p.row ? 'row' : 'column'};
`
@DanielRosenwasser
Copy link
Member Author

@MartinJohns it's on my backlog, sorry. 😞

@sergey-shandar Not sure what you're asking about, but any problems or authoring questions should be filed as a new issue or as a StackOverflow question respectively.

@microsoft microsoft locked as resolved and limited conversation to collaborators Apr 24, 2018
@microsoft microsoft unlocked this conversation Apr 24, 2018
@sergey-shandar
Copy link
Contributor

@DanielRosenwasser I think you mean @sergeysova.

existentialism pushed a commit to babel/babel that referenced this pull request Jul 26, 2018
| Q                        | A
| ------------------------ | ---
| Fixed Issues?            | #7747 (partly)
| Patch: Bug Fix?          | 
| Major: Breaking Change?  | 
| Minor: New Feature?      | Yes
| Tests Added + Pass?      | Yes
| Documentation PR         |
| Any Dependency Changes?  |
| License                  | MIT

@JamesHenry This changes the AST format. CC @DanielRosenwasser for review.
Supports parsing type arguments on tagged template calls.
Should wait on microsoft/TypeScript#23430 to be merged so we're sure we have the final syntax.
@microsoft microsoft locked and limited conversation to collaborators Jul 31, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
9 participants