如何从打字稿中的导入文件中推断类型

发布于 2025-01-25 11:36:49 字数 1493 浏览 6 评论 0 原文

我有一个棘手的打字稿问题,我似乎无法解决。我想根据通用来推断值。但是,捕获是推断类型需要来自导入的文件。

在一个示例中解释这是我的通用功能。

import { DocumentNode, print } from "graphql"

type GraphqlRequest = <Query, Variables = {}>({ query, variables }: { query: DocumentNode, variables?: Variables }) => Promise<Query>

const fetchQuery: GraphqlRequest = async ({ query, variables = {} }) => {
  return database(JSON.stringify({ query: print(query), variables }))
}

我以下面的方式使用它:

import { UserBlogItems, UserBlogItemsQuery, UserBlogItemsQueryVariables } from "~/graphql"

fetchQuery<UserBlogItemsQuery, UserBlogItemsQueryVariables>({ query: UserBlogItems, variables: { user } })

哪种效果很好,因为我可以在变量和查询响应中查看正确的类型。


我想做的是只需包含 documenode ,并让打字稿查找 QUERY> QUERY 变量 >从“〜/graphql”文件。这样,我可以使该功能看起来更加清洁。

import { UserBlogItems } from "~/graphql"

fetchQuery({ query: UserBlogItems, variables: { user }})

我可以始终确保〜/graphql 文件中的100%。格式将为:

  • > documentnode:[myquery] (例如 userblogitems
  • 查询:[myquery] QUERY (例如 userblogitemsquery
  • code>变量:[[[ myquery] queryVariables (例如 userblogitemsqueryvariables

这在打字稿中是否可以吗?

I have a tricky Typescript problem I can't seem to solve. I would like to infer values based on a generic. However, the catch is the inferred types needs to come from an imported file.

To explain in an example, this is my generic function.

import { DocumentNode, print } from "graphql"

type GraphqlRequest = <Query, Variables = {}>({ query, variables }: { query: DocumentNode, variables?: Variables }) => Promise<Query>

const fetchQuery: GraphqlRequest = async ({ query, variables = {} }) => {
  return database(JSON.stringify({ query: print(query), variables }))
}

And I use it in the following way:

import { UserBlogItems, UserBlogItemsQuery, UserBlogItemsQueryVariables } from "~/graphql"

fetchQuery<UserBlogItemsQuery, UserBlogItemsQueryVariables>({ query: UserBlogItems, variables: { user } })

Which works great because I can get the correct types checking on the variables and the query response.


What I would like to be able to do is just include the DocumentNode and have Typescript look up the Query and Variables from the "~/graphql` file. This way I can have the function look a lot cleaner.

import { UserBlogItems } from "~/graphql"

fetchQuery({ query: UserBlogItems, variables: { user }})

I can ensure 100% always that in the ~/graphql file the format will be:

  • DocumentNode: [myquery] (eg UserBlogItems)
  • Query: [myquery]Query (eg UserBlogItemsQuery)
  • Variables: [myquery]QueryVariables (eg UserBlogItemsQueryVariables)

Is this possible in Typescript?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

新雨望断虹 2025-02-01 11:36:50

为了为我们推断这些仿制药,我们需要将每个文档节点映射到其各自的查询和查询变量类型。这是通过这样的元素列表来完成的:

type QueryTypeMap = [
    [UserBlogItems, UserBlogItemsQuery, UserBlogItemsQueryVariables],
    [UserInfo, UserInfoQuery, UserInfoQueryVariables],
];

第一个元素是文档节点,其次是查询类型,最后是查询变量。您也可以按以下方式编写它并编辑我们以后访问它们以更加明确:

type QueryTypeMap = [
    [UserBlogItems, { query: UserBlogItemsQuery; vars: UserBlogItemsQueryVariables }],
    // ...
];

接下来,我们创建的类型将根据给定文档节点为我们检索类型:

type FindQueryTypes<T> = {
    [K in keyof QueryTypeMap & string]:
        // If this value is a tuple (AKA one of our map entries)
        QueryTypeMap[K] extends ReadonlyArray<unknown>
            // And the document node matches with the document node of this entry
            ? T extends QueryTypeMap[K][0]
                // Then we have found our types
                ? QueryTypeMap[K]
                // Or not
                : never
            // Not even a map entry
            : never;
}[keyof QueryTypeMap & string];

为什么 keyof queryTyPemap&amp&amp;字符串 queryTyPemap 是一个元组,这意味着 QUERYTYPEMAP 包括 numbern 类型,这会弄乱我们的类型,试图找到正确的查询类型。因此,我们只能通过允许使用&amp;的字符串键来排除它。字符串

我们还必须注意, k queryTyPemap 的键是 数组的任何键。因此,例如,它可以是 indexof 剪接。这就是为什么我们必须首先检查 queryTyPemap [k] 是元组(这意味着它是我们地图的条目,而不是内置的数组属性)。

最后,我们在函数类型中使用此类型:

type GraphQLRequest = <
    D extends typeof DocumentNode,
    Types extends QueryTypeMap[number] = FindQueryTypes<InstanceType<D>>
>(
    { query, variables }: { query: D, variables?: Types[2] }
) => Promise<Types[1]>;

typeScript将推断通用类型 d (任何扩展documentnode的类构造函数是 type type documentof docrightnode is),这意味着我们可以在第二个通用类型中直接使用它而不会麻烦。

我们将类型限制为类型 queryTyPemap [number] ,这只能是 queryTyPemap 的任何条目。现在,我们从上面的类型将此通用价值赋予了它的价值。

您会看到那里有一个额外的 InstanceType ,这是因为 d 不是 userblogitems userInfo 。它实际上是 typeof userBlogItems type oferInfo (构造函数)。要获取实例类型,我们使用恰当命名的内置类型 InstanceType

之后,我们坐着很漂亮。我们有正确的查询类型。现在我们只使用它们。

对于变量的类型,我们使用类型[2] 获取查询变量类型。

对于返回类型,我们使用类型[1] 获取查询类型。

Once again, the playground, so you can tinker with this yourself now that you've got some understanding of it.

For the function to infer these generics for us, we need to map each document node to its respective query and query variables types. This is done with a list of tuples like this:

type QueryTypeMap = [
    [UserBlogItems, UserBlogItemsQuery, UserBlogItemsQueryVariables],
    [UserInfo, UserInfoQuery, UserInfoQueryVariables],
];

The first element is the document node, followed by the query type, and finally the query variables. You could also write it as this and edit where we access them later to be more explicit:

type QueryTypeMap = [
    [UserBlogItems, { query: UserBlogItemsQuery; vars: UserBlogItemsQueryVariables }],
    // ...
];

Next, we create the type that will retrieve the types for us based on the given document node:

type FindQueryTypes<T> = {
    [K in keyof QueryTypeMap & string]:
        // If this value is a tuple (AKA one of our map entries)
        QueryTypeMap[K] extends ReadonlyArray<unknown>
            // And the document node matches with the document node of this entry
            ? T extends QueryTypeMap[K][0]
                // Then we have found our types
                ? QueryTypeMap[K]
                // Or not
                : never
            // Not even a map entry
            : never;
}[keyof QueryTypeMap & string];

Why is there keyof QueryTypeMap & string? QueryTypeMap is a tuple, which means keyof QueryTypeMap includes the number type, and that would mess with our type trying to find the correct query type. So we can exclude it by only allowing string keys with & string.

We must also be mindful that K, the key of QueryTypeMap, is any key of an array. So for example, it could be indexOf or splice. That is why we must first check if QueryTypeMap[K] is a tuple (meaning it's an entry of our map and not a built-in array property).

Lastly, we use this type in our function type:

type GraphQLRequest = <
    D extends typeof DocumentNode,
    Types extends QueryTypeMap[number] = FindQueryTypes<InstanceType<D>>
>(
    { query, variables }: { query: D, variables?: Types[2] }
) => Promise<Types[1]>;

TypeScript will infer the generic type D (any class constructor that extends DocumentNode is what typeof DocumentNode is) for us, which means we can directly use it without hassle in the second generic type.

We constrain Types to the type QueryTypeMap[number], which is saying it can only be any entry of QueryTypeMap. Now we give this generic its value with our type from above.

You'll see that there's an extra InstanceType there, and that's because D is not UserBlogItems or UserInfo. It's actually typeof UserBlogItems or typeof UserInfo (the constructors). To get the instance types, we use the aptly named built-in type InstanceType.

And after that, we're sitting pretty. We've got the correct query types. Now we just use them.

For the type of variables, we use Types[2] to get the query variables type.

For the return type, we use Types[1] to get the query type.

Once again, the playground, so you can tinker with this yourself now that you've got some understanding of it.

浪菊怪哟 2025-02-01 11:36:50

而不是通过使用命名惯例来推断类型,而不是做一些黑暗的魔法。您为什么不将从〜/graphQl导出的方式标准化。
这样:

//==== ~/graphql ====

interface UserBlogItems extends DocumentNode{
  foo: string;
} 
type UserBlogItemsQuery = ASTNode & {
  bar: string;
} 
interface UserBlogItemsQueryVariables{
  user:string;
}

export interface UserBlogEntity {
    documentNode:UserBlogItems;
    query: UserBlogItemsQuery;
    variables: UserBlogItemsQueryVariables;
}

//-----------------

定义这样的提取物:

interface GraphqlRequestData {
    documentNode:DocumentNode;
    query: ASTNode;
    variables?: Record<string,any>;
}


const fetchQuery = async <T extends GraphqlRequestData>({ query, variables }: {query:T['documentNode'], variables?:T['variables']}):Promise<T['query']> => {
  return database(JSON.stringify({ query: print(query), variables })) as Promise<T['query']>;
};

因此,您最终可以这样使用:

const userBlogItems :UserBlogItems = {
    foo: `test`
} as UserBlogItems;
const userBlogVars: UserBlogItemsQueryVariables = {
    user: 'user_name'
}


const run = async()=>{
    const returnQuery = await fetchQuery<UserBlogEntity>({ query: userBlogItems, variables: userBlogVars });
    console.log(returnQuery.bar)
}

这是Playgorund链接:Playground

Instead of doing some dark magic to infer types by using naming conventions. Why don't you normalize the way you export thing from ~/graphql.
like such:

//==== ~/graphql ====

interface UserBlogItems extends DocumentNode{
  foo: string;
} 
type UserBlogItemsQuery = ASTNode & {
  bar: string;
} 
interface UserBlogItemsQueryVariables{
  user:string;
}

export interface UserBlogEntity {
    documentNode:UserBlogItems;
    query: UserBlogItemsQuery;
    variables: UserBlogItemsQueryVariables;
}

//-----------------

And the define the fetchQuery like this:

interface GraphqlRequestData {
    documentNode:DocumentNode;
    query: ASTNode;
    variables?: Record<string,any>;
}


const fetchQuery = async <T extends GraphqlRequestData>({ query, variables }: {query:T['documentNode'], variables?:T['variables']}):Promise<T['query']> => {
  return database(JSON.stringify({ query: print(query), variables })) as Promise<T['query']>;
};

So you finaly can use it like this:

const userBlogItems :UserBlogItems = {
    foo: `test`
} as UserBlogItems;
const userBlogVars: UserBlogItemsQueryVariables = {
    user: 'user_name'
}


const run = async()=>{
    const returnQuery = await fetchQuery<UserBlogEntity>({ query: userBlogItems, variables: userBlogVars });
    console.log(returnQuery.bar)
}

and here is the playgorund link: Playground

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文