【问题标题】:Typescript: Mapped return type based on keys present in argument打字稿:基于参数中存在的键的映射返回类型
【发布时间】:2019-07-15 12:33:20
【问题描述】:

假设我正在编写一些数据库接口,我可以使用它一次提交多个查询,作为由表名作为键的对象,并期望接收一个对象,其结果与查询中的键相同。

interface Query {
    users?: UsersQuery
    products?: ProductsQuery
    orders?: OrdersQuery
}
interface Result {
    users?: User[]
    products?: Product[]
    orders?: Order[]
}

declare function readData(query: Query): Result;

有没有什么方法可以根据传入的对象中实际存在的键来更具体地确定返回类型?

似乎我很接近这样的事情:

declare function readData<T extends Query>(query: T): { [K in keyof T]: T[K] }

但不是使用T[K] 的映射返回类型,我需要它使用Result[K],它不能编译(使用error TS2536: Type 'K' cannot be used to index type 'Result')。根据文档,我猜这是因为 T[K] 以外的任何东西都不是同态的,尽管我对此不是很清楚。

在使用映射类型时我在这里缺少什么,或者有其他方法可以做到这一点吗?

【问题讨论】:

  • 总是有助于纠正错误。 “不编译”不是表达您面临的问题的好方法。包括您尝试的全部内容和实际错误消息。 Result[K] 方法是在正确的道路上..
  • 谢谢,我添加了编译器错误以供将来参考。就像您输入的答案一样,这是因为 T 可能具有 Result 上可能不存在的额外属性。

标签: typescript


【解决方案1】:

在这种情况下,类型如何工作的一个更意想不到的后果是,由于T extends Query 它不仅可以具有Query 的属性,而且只要有一些重叠,它就可以具有任何可能的键不适用于检查查询对象是否正确:

declare function readData<T extends Query>(query: T): { [K in keyof T]: T[K] }

readData({
    bla: 1, // not in Query but the compiler is fine, no excess property checks since T just has to extend Query
    orders: null
})

这也是您收到T 错误的原因,除了查询中的键之外,可能还有其他键。

一种选择是指定函数的参数是QueryPick,选择的键是函数的类型参数:

declare function readData<K extends keyof Query>(query: Pick<Query, K>): { [P in K]: Result[K] }


readData({
    users: null!,
    bla: 0 // error now
})

虽然这确实按预期进行类型检查,但问题是它不会在对象字面量的键上提供代码完成,这是不幸的。

如果我们添加与 Query 的部分的交集,我们会返回良好的代码完成,并在 K 中捕获传入的实际键(尽管它们可以取消定义,但您可能已经在检查)

declare function readData<K extends keyof Query>(query: Pick<Query, K> & Partial<Query>): { [P in K]: Result[K] }


readData({
    users: null!,
    // we get suggestions here
})

【讨论】:

    【解决方案2】:

    首先将UserQueryProductQuery 等泛化为使用通用的通用基类型。例如:

    // Entity definitions
    type User = { userId: string };
    type Product = { productId: string };
    type Order = { orderId: string };
    
    type EntityQuery<T> = { [K in keyof T]: T[K] }
    type ResultOf<T> = T extends EntityQuery<infer I> ? I[] : undefined;
    

    然后像这样定义你的Query 接口:

    interface Query {
        users?: EntityQuery<User>
        products?: EntityQuery<Product>
        orders?: EntityQuery<Order>
    }
    

    现在您可以像这样将readData 设为通用:

    declare function readData<T extends Query>(query: T): { [K in keyof T]: ResultOf<T[K]> };
    

    【讨论】:

    • 酷,我不知道 infer 关键字。我没有在我的问题中提供有关查询类型的任何详细信息,但是对于数据库接口的示例,查询可能不一定是它们返回的实体类型的 1:1,例如,如果您只能查询表中存在的字段,或者如果您想要一个允许传递匹配多个事物的数组的查询接口。在这种情况下,将无法从查询中提取返回类型应该是什么。
    【解决方案3】:

    你在正确的轨道上!

    我会这样做:

    // Create a mapping between the query type used, and the result type expected:
    type QueryResult<T> =
        T extends UsersQuery ? User[] :
        T extends ProductsQuery ? Product[] :
        T extends OrdersQuery ? Order[] :
        never
    ;
    
    // Same as what you started with, but using our new QueryResult<T> mapping:
    declare function readData<T extends Query>(query: T): { [K in keyof T]: QueryResult<T[K]> };
    

    这应该会根据您传入的内容为您提供正确的类型。

    作为参考,我使用以下接口对此进行了测试:

    interface UsersQuery {
        user_id: string;
    }
    
    interface ProductsQuery {
        product_id: string;
    }
    
    interface OrdersQuery {
        order_id: string;
    }
    
    interface User {
        _id: string;
        userName: string;
    }
    
    interface Product {
        _id: string;
        productName: string;
    }
    
    interface Order {
        _id: string;
        orderName: string;
    }
    
    interface Query {
        users?: UsersQuery;
        products?: ProductsQuery;
        orders?: OrdersQuery;
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-11-19
      • 2019-06-01
      • 2019-10-25
      • 1970-01-01
      • 2021-05-16
      相关资源
      最近更新 更多