TypeScript [功能请求] 支持标记模板表达式的自定义类型

jljoyd4f  于 6个月前  发布在  TypeScript
关注(0)|答案(7)|浏览(56)

问题陈述
在TypeScript中嵌入带标签的模板表达式是很常见的。例如,你可能会使用SQL:

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

你也会在GraphQL中看到类似的模式:

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

目前,没有办法提供关于queryGraph / querySQL返回内容的类型信息。在这里边界处有很多潜在的类型信息丢失。
显然,我不希望TypeScript读取GraphQL或SQL模式,因为那超出了范围。我希望有一种方法作为模块作者提供这些信息。

建议

我认为所需要的只是编写一个插件来回答“给定一个带标签的模板表达式,它返回什么类型”。这只会影响类型检查器。它不会改变生成的代码或添加任何新的语法。
将添加一个新的compilerConfig选项,名为taggedTemplateHandlers,它接受一个指向TypeScript文件的路径数组。一个示例可能如下所示:

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
          )
        }))
      ])
    );
  }
};

然后你可以像这样定义querySQL:

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

语言特性清单

  • 语义 - 无新更改
  1. 当类型检查器遇到带标签的模板时
  2. 查看是否为该标签注册了taggedTemplateHandlers
  3. 如果存在,则调用该taggedTemplateHandler
  4. 使用由taggedTemplateHandler返回的TypeNode替换默认行为。
  • 其他
  • 我预计会有一些性能影响,但希望由于插件和编译器都是用TypeScript编写的事实,这将是最小的影响。
  • 理想情况下,这种信息将用于自动完成助手以及类型检查器中,我不知道是否需要额外的工作。

我很乐意尽我所能来帮助实现这个功能,但我需要一些关于从哪里开始的指导。
P.S. 能否传递表达式的类型,而不是实际的表达式本身?

edqdpe6u

edqdpe6u1#

这与#3136非常相关。坦率地说,我不知道我们准备实施类似的东西有多充分。我认为它会非常有用,但这是一个相当大的改变,我不确定我们能否在不久的将来承诺任何时间,但我会愿意听取其他人的想法。

xxhby3vn

xxhby3vn2#

是的,这与#3136密切相关,但我认为这是一个更强的使用案例。您始终可以使用代码生成来为远程服务生成强类型API。关键区别在于我们将允许用户坚持使用原生语言。

ru9i0ody

ru9i0ody3#

这似乎最适合语言服务插件。@mjbvz最近做了很多很棒的工作,为标记模板字面量提供智能感知和语法高亮。
@mjbvz:你知道有没有办法使用当前的语言服务插件API为标记模板字面量提供返回类型安全吗?还是需要将其完全添加到API中作为新功能?
我正在尝试拼凑一个更简单(且功能完整)的VS Code扩展+语言服务插件用于GraphQL。

vql8enpb

vql8enpb4#

TypeScript插件可以修改TypeScript的typing信息。例如,您可以使用typings.json文件来管理和安装TypeScript定义。此外,一些typescript-eslint规则利用TypeScript类型检查API的强大功能来提供对代码更深入的了解。

zkure5ic

zkure5ic5#

感谢回复 @mjbvz - 我目前的思路是开发一个与语言服务接口的VS Code插件,用于创建类型定义并将它们插入到当前文档中。
例如:
Foo.ts

const foo = graphql`
query { ... }
`

VS Code插件会根据给定的查询找出类型信息,然后在原始文件的子目录中创建一个新文件,其中定义了类型接口。因此,在这种情况下,您最终得到以下文件结构:

Foo.ts
__generated__/Foo.queries.d.ts

原始文件(Foo.ts)通过VS Code插件进行修改,以添加相关的导入和引用,如下所示:

import { QueryResult } from './__generated__/Foo.queries'
const foo = <QueryResult>graphql`
  query { ... }
`

虽然有点复杂,但最终结果确实可以很好地保证类型安全。希望尽快完善并发布。

cclgggtu

cclgggtu6#

我想重新审视这个问题。我使用了像@jayrylan提到的代码生成解决方案,但我觉得它们有点太复杂了。设置起来很费劲,如果类型不正确或无法生成,还得运行监视器进程或安装编辑器插件并尝试找出问题所在,这让人很恼火。我使用的生成器也都依赖于查询和片段名称不冲突,因为生成器将它们全部放入同一个巨大的类型文件中。即使他们理解TypeScript本身,我也觉得教育新项目贡献者关于发生了什么(为什么要从这里导入查询,但从那里导入类型定义)是一个速度障碍。
而且,最核心的是,感觉有点...不对...要让TypeScript的类型承诺与GraphQL的强类型API一起工作,但没有一堆胶带是不行的。
在找到这个问题之前,我最初的解决方案是发明一个新的关键字来表示从泛型动态生成的类型。

generatedType GraphQLResult<TDoc extends GraphQLDocument> = (doc: TDoc) => {
  const ast = parseGraphQLDocument(doc);
  return walkASTAndConvertToTypeScriptTypes(ast);
};

function graphQLRequest<TDoc extends GraphQLDocument>(doc: TDoc): GraphQLResult<TDoc> {
  // ...
}

显然,这里有很多困难。可以推测TypeScript在处理过程中会从代码中删除generatedType定义。如果代码引用其他函数(如我的代码),不确定它是否会知道是否也要删除这些函数。我还包含了一个指向解析函数的引用,该函数可能来自库。我不假装了解TypeScript是如何工作的,或者它是否能在类型检查期间从该库导入该函数以解决此类型。
然而,我总体上喜欢这种方法,因为生成的类型可以打包成一个库(如制作GraphQL请求的库),用户甚至都不需要考虑它就可以获得完整的类型。
我还认为这个概念可能不仅局限于模板。例如,为这个函数添加类型:

function camelCaseKeys<T extends {}>(t: T): CamelCased<T> { /* ... */ }

camelCaseKeys({ A: 0, FooBar: 1 });
// { a: 0, fooBar: 1 }

我知道这个功能可能会非常困难,甚至比我意识到的还要困难。也许甚至不可能用现在的TS实现。但我认为这也是无缝添加更多内容的一个巨大进步。

csbfibhn

csbfibhn7#

有任何消息吗?

相关问题