简体   繁体   English

有时根据参数存在于打字稿对象上的键

[英]Keys that sometimes exist on a typescript object depending on parameters

I have an object that takes a lot of time to construct fully (because it requires querying multiple database tables), so clients of that object specify which parts they need.我有一个需要花费大量时间才能完全构建的对象(因为它需要查询多个数据库表),因此该对象的客户端指定他们需要哪些部分。 For instance, let's say that there's a User object that sometimes has Purchases and sometimes has Friends.例如,假设有一个 User 对象,有时有 Purchases 有时有 Friends。 Right now, I'm using optional properties for that:现在,我为此使用了可选属性:

interface User {
  id: number
  purchases?: Purchase[]
  friends?: Friend[]
  friendsOfFriends?: Friend[]
}

And clients of a User could say getUser(['purchases']) to get a user where the purchases key is defined.用户的客户可以说getUser(['purchases'])来获取定义了purchases键的用户。

However, that means that any time I use purchases , friends , or friendsOfFriends , I need to tell typescript that the type is there (eg, user.purchases![0] or user.purchases && user.purchases[0] ), which is a little annoying.然而,这意味着任何时候我使用purchasesfriendsfriendsOfFriends ,我都需要告诉friendsOfFriends类型在那里(例如, user.purchases![0]user.purchases && user.purchases[0] ),这有点烦。

Is there some way to tell Typescript that the passed in parameter determines which keys are present on the returned value?有没有办法告诉 Typescript 传入的参数决定了返回值上存在哪些键? Eg:例如:

  • getUser([]) returns an {id: number} getUser([])返回一个{id: number}
  • getUser(['purchases']) returns an {id: number; purchases: Purchase[]} getUser(['purchases'])返回一个{id: number; purchases: Purchase[]} {id: number; purchases: Purchase[]}
  • getUser(['network']) returns an {id: number; friends: Friend[]; friendsOfFriends: Friend[]} getUser(['network'])返回一个{id: number; friends: Friend[]; friendsOfFriends: Friend[]} {id: number; friends: Friend[]; friendsOfFriends: Friend[]} {id: number; friends: Friend[]; friendsOfFriends: Friend[]} -- note that network gets us friends and friendsOfFriends {id: number; friends: Friend[]; friendsOfFriends: Friend[]} -注意, network会让我们friendsfriendsOfFriends
  • getUser(['purchases', 'network']) returns an {id: number; purchases: Purchase[]; friends: Friend[]; friendsOfFriends: Friend[]} getUser(['purchases', 'network'])返回一个{id: number; purchases: Purchase[]; friends: Friend[]; friendsOfFriends: Friend[]} {id: number; purchases: Purchase[]; friends: Friend[]; friendsOfFriends: Friend[]}

In the real case, there are more than 2 possible keys to include, and I don't want to make combinatorially many overloaded types.在实际情况下,有超过 2 个可能的键可以包含,而且我不想组合许多重载类型。

Is this possible in Typescript?这在打字稿中可能吗?

Thanks!谢谢!

I'm going to leave the implementation of getUser() us you, as well as convincing the compiler that your implementation meets the type definition.我将把getUser()的实现留给我们,同时让编译器相信你的实现符合类型定义。 That is, I'm acting like getUser() is out in pure JavaScript land, and I'm just coming up with its type declaration so that the compiler can correctly handle calls to it.也就是说,我的行为就像getUser()在纯 JavaScript 领域中一样,我只是想出了它的类型声明,以便编译器可以正确处理对它的调用。

First, the compiler needs some way of knowing about the mapping between arguments to getUser() and which sets of User keys they pick out.首先,编译器需要某种方式来了解getUser()参数之间的映射以及它们选择的User键集。 Something like this, given your example:像这样的东西,给你的例子:

interface KeyMap {
    purchases: "purchases",
    network: "friends" | "friendsOfFriends"
}

Armed with that, we need to tell the compiler how to calculate UserType<K> for some set of keys K from KeyMap .与武装,我们需要告诉编译器如何计算UserType<K>对于一些组键KKeyMap Here's one way to do it:这是一种方法:

type UserType<K extends keyof KeyMap> =
    User & Pick<Required<User>, KeyMap[K]> extends 
    infer O ? { [P in keyof O]: O[P] } : never;

The important bit is User & Pick<Required<User>, KeyMap[K]> : The result is always going to be a User so we include that in an intersection .重要的一点是User & Pick<Required<User>, KeyMap[K]> :结果总是一个User所以我们把它包含在一个交叉点中 We also take KeyMap[K] , the User keys pointed out by K , and Pick these properties from Required<User> .我们还KeyMap[K]中, User密钥通过指出K ,和Pick从这些特性Required<User> The Required<T> type makes all optional keys into required ones. Required<T>类型使所有可选键都成为必需的。

The next bit that starts extends infer O... is really just using a conditional type inference trick to take the ugly type User & Pick... and convert it into a single object type with spelled out properties.下一个开始extends infer O...实际上只是使用条件类型推断技巧来获取丑陋的类型User & Pick...并将其转换为具有拼写属性的单个对象类型。 If you prefer to see User & Pick<Required<User>, "purchases"> instead of the object types below, you can remove everything starting with extends there.如果您更喜欢看到User & Pick<Required<User>, "purchases">而不是下面的对象类型,您可以删除所有以extends开头的内容。

Finally, the getUser() function typing looks like this:最后, getUser()函数类型如下所示:

declare function getUser<K extends keyof KeyMap>(keys: K[]): UserType<K>;

It takes an array of KeyMap keys and returns UserType<K> for those keys.它接受一组KeyMap键并为这些键返回UserType<K>


Let's make sure it works, using your example calls:让我们使用您的示例调用确保它有效:

const user = getUser([]);
/* const user: {
    id: number;
    purchases?: Purchase[] | undefined;
    friends?: Friend[] | undefined;
    friendsOfFriends?: Friend[] | undefined;
} */

const userWithPurchases = getUser(['purchases']);
/* const userWithPurchases: {
    id: number;
    purchases: Purchase[];
    friends?: Friend[] | undefined;
    friendsOfFriends?: Friend[] | undefined;
} */

const userWithNetwork = getUser(['network']);
/* const userWithNetwork: {
    id: number;
    purchases?: Purchase[] | undefined;
    friends: Friend[];
    friendsOfFriends: Friend[];
} */

const userWithEverything = getUser(['purchases', 'network']);
/* const userWithEverything: {
    id: number;
    purchases: Purchase[];
    friends: Friend[];
    friendsOfFriends: Friend[];
} */

Those types look right to me.这些类型对我来说很合适。

Playground link to code Playground 链接到代码


Well, looking over your example again, you didn't have the optional properties included in the output as optional;好吧,再次查看您的示例,您没有将可选属性包含在输出中作为可选; they were just omitted.他们只是被省略了。 If you really want to see that match your types exactly it's a bit more involved:如果你真的想看到与你的类型完全匹配,那就有点复杂了:

type RequiredKeys<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? never : K }[keyof T];

type UserType<K extends keyof KeyMap> =
    Pick<User, RequiredKeys<User>> & Pick<Required<User>, KeyMap[K]> extends
    infer O ? { [P in keyof O]: O[P] } : never;

declare function getUser<K extends keyof KeyMap>(keys: K[]): UserType<K>;


const user = getUser([]);
/* const user: {
    id: number;
} */

const userWithPurchases = getUser(['purchases']);
/* const userWithPurchases: {
    id: number;
    purchases: Purchase[];
} */

const userWithNetwork = getUser(['network']);
/* const userWithNetwork: {
    id: number;
    friends: Friend[];
    friendsOfFriends: Friend[];
} */

const userWithEverything = getUser(['purchases', 'network']);
/* const userWithEverything: {
    id: number;
    purchases: Purchase[];
    friends: Friend[];
    friendsOfFriends: Friend[];
} */

Playground link to this code 此代码的游乐场链接


Whichever way you want to go, or some other way, is up to you.无论您想走哪条路,还是其他方式,都取决于您。 Okay, hope that helps;好的,希望有帮助; good luck!祝你好运!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM