Conversation

DanielRosenwasser

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

Beautiful! Thank you :)

@Havret

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

@MartinJohns

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

@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

No update of the spec. :-(

@sergeysova

What about passed functions?

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

@DanielRosenwasser

@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.

@microsoftmicrosoft locked as resolved and limited conversation to collaborators Apr 24, 2018
@microsoftmicrosoft unlocked this conversation Apr 24, 2018
@sergey-shandar

@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)
| : 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.
@microsoftmicrosoft locked and limited conversation to collaborators Jul 31, 2018
Sign up for free to subscribe to this conversation on . Already have an account? Sign in.
None yet
None yet

Successfully merging this pull request may close these issues.