Skip to content

[feature request] Support Custom Types for Tagged Template Expressions #16551

Open
@ForbesLindesay

Description

@ForbesLindesay

Problem Statement

It is common to embed queries in tagged template expressions within typescript. For example, you might use sql:

const result = await querySQL(sql`SELECT * FROM users WHERE id = ${userID}`);

You also see a similar pattern with graphql:

const result = await queryGraph(graphql`query { user { id, name } }`);

Currently, there is no way to provide type information about what is returned by queryGraph/querySQL. A lot of potential type information is lost at the boundary here.

I obviously don't expect typescript to read the graphql or SQL schema, as that's way out of scope. What I would like is a way to provide this information as a module author.

Proposal

I believe that all that's needed is a way to write plugins that answer the question "given a tagged template expression, what type does it return". This would only affect the type checker. It would not change generated code or add any new syntax.

A new compilerConfig option called taggedTemplateHandlers would be added, that would take an array of paths to typescript files. An example might look something like:

import {parseSqlQuery, convertSqlTypeToTypeScriptType} from 'sql-helpers';
import * as ts_module from "typescript/lib/tsserverlibrary";

export default {
  tag: 'sql',
  templateHandler(modules: {typescript: typeof ts_module}, strings: string[], ...expressions: Array<ts_module.Expression>) {
    const ts = modules.typescript;

    const fields = parseSqlQuery(strings.join('"EXPRESSION"'));
    return ts.createTypeReferenceNode(
      ts.createIdentifier('SqlQuery'), // typeName
      ts.createNodeArray([ // typeArguments
        ts.createTypeLiteralNode(Object.keys(fields).map(field => {
          return ts.createPropertySignature(
            ts.createIdentifier(field),
            undefined, // question token
            convertSqlTypeToTypeScriptType(fields[field]), // type
            undefined, // initializer
          )
        }))
      ])
    );
  }
};

You could then define querySQL like:

define function querySQL<TResult>(query: SqlQuery<TResult>): Promise<TResult>;

Language Feature Checklist

  • Syntactic - no new changes
  • Semantic
    • When a tagged template is encountered by the type-checker
      1. See if a taggedTemplateHandlers has been registered for that tag
      2. Call that taggedTemplateHandler if one exists.
      3. Use the TypeNode returned by the taggedTemplateHandler in place of the default behaviour.
  • Emit - no new changes
  • Compatibility - no new syntax is added/changed, so it should be fully backwards/forwards compatible.
  • Other
    • I expect there will be some performance impact, but hopefully the fact that both plugins and the compiler are written in typescript should make this minimal.
    • Ideally, this information would be used for autocomplete helpers as well as in the typechecker, I do not know if that would require extra work.

I'm happy to do my best to help implement this, but I would need some pointers on where to start.

P.S. would it be possible to pass in the type of the expressions, in place of the actual expressions themselves?

Metadata

Metadata

Assignees

No one assigned

    Labels

    In DiscussionNot yet reached consensusSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions