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

Projects
None yet
9 participants
@DanielRosenwasser
Member

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

This comment has been minimized.

lucasterra commented Apr 16, 2018

Beautiful! Thank you :)

@Havret

This comment has been minimized.

Havret commented Apr 16, 2018

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

@MartinJohns

This comment has been minimized.

MartinJohns commented Apr 16, 2018

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;

This comment has been minimized.

@mhegazy

mhegazy Apr 17, 2018

Contributor

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

This comment has been minimized.

@mhegazy

mhegazy Apr 17, 2018

Contributor

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

This comment has been minimized.

@DanielRosenwasser

DanielRosenwasser Apr 17, 2018

Member

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) {

This comment has been minimized.

@ajafff

ajafff Apr 17, 2018

Contributor

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.

This comment has been minimized.

@DanielRosenwasser
@DanielRosenwasser

This comment has been minimized.

Member

DanielRosenwasser commented Apr 17, 2018

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

@DanielRosenwasser DanielRosenwasser referenced this pull request Apr 19, 2018

Closed

TypeScript 2.9 meta-issue #603

2 of 3 tasks complete
@@ -17749,7 +17749,11 @@ namespace ts {
let typeArguments: NodeArray<TypeNode>;
if (!isTaggedTemplate && !isDecorator && !isJsxOpeningOrSelfClosingElement) {
if (isTaggedTemplate) {

This comment has been minimized.

@weswigham

weswigham Apr 19, 2018

Member

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)

@DanielRosenwasser DanielRosenwasser merged commit 84b1291 into master Apr 19, 2018

8 checks passed

TypeScript Test Run typescript_node.6 Build finished.
Details
TypeScript Test Run typescript_node.8 Build finished.
Details
TypeScript Test Run typescript_node.stable Build finished.
Details
ci/circleci: node6 Your tests passed on CircleCI!
Details
ci/circleci: node8 Your tests passed on CircleCI!
Details
ci/circleci: node9 Your tests passed on CircleCI!
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
license/cla All CLA requirements met.
Details

@DanielRosenwasser DanielRosenwasser deleted the taggedTemplateTypeArguments branch Apr 19, 2018

@MartinJohns

This comment has been minimized.

MartinJohns commented Apr 20, 2018

No update of the spec. :-(

@sergeysova

This comment has been minimized.

sergeysova commented Apr 20, 2018

What about passed functions?

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

This comment has been minimized.

Member

DanielRosenwasser commented Apr 24, 2018

@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

This comment has been minimized.

Contributor

sergey-shandar commented Apr 24, 2018

@DanielRosenwasser I think you mean @sergeysova.

existentialism added a commit to babel/babel that referenced this pull request Jul 26, 2018

TypeScript: Support type arguments on tagged templates (#7754)
| 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.