Typescript不能使用定义为T类型键子集的键来索引类型T



注:根据@GarlefWegart的回复,最终解决方案见下文编辑

我正在尝试为动态GraphQL查询结果编写泛型类型(更有趣的是,因为我确信这些可能已经存在于某个地方)。

我很接近了,但是碰到了一个奇怪的问题。完整的代码在这里的游乐场,并复制如下。

问题集中在使用派生对象的键索引对象,这应该工作,但由于某种原因失败。注意,在Result定义中,我不能使用K索引T,即使K被定义为U的键,U被定义为T属性的子集。这意味着U的所有键必然也是T的键,因此用U的任何键索引T应该是安全的。然而,Typescript拒绝这样做。

type SimpleValue = null | string | number | boolean;
type SimpleObject =  { [k: string]: SimpleValue | SimpleObject | Array<SimpleValue> | Array<SimpleObject> };
type Projection<T extends SimpleObject> = {
[K in keyof T]?:
T[K] extends SimpleObject
? Projection<T[K]>
: T[K] extends Array<infer A>
? A extends SimpleObject
? Projection<A>
: boolean
: boolean;
};
type Result<T extends SimpleObject, U extends Projection<T>> = {
[K in keyof U]:
U[K] extends false
? never                            // don't return values for false keys
: U[K] extends true
? T[K]                         // return the original type for true keys
// ^^vv All references to T[K] throw errors
: T[K] extends Array<infer A>
? Array<Result<A, U[K]>>   // Return an array of projection results when the original was an array
: Result<T[K], U[K]>;      // Else it's an object, so return the projection result for it
}

type User = {
id: string;
email: string;
approved: string;
address: {
street1: string;
city: string;
state: string;
country: {
code: string;
allowed: boolean;
}
};
docs: Array<{
id: string;
url: string;
approved: boolean;
}>
}
const projection: Projection<User> = {
id: false,
email: true,
address: {
country: {
code: true
}
},
docs: {
id: true,
url: true
}
}
const result: Result<User, typeof projection> = {
email: "me@us.com",
address: {
country: {
code: "US"
}
},
docs: [
{
id: "1",
url: "https://abcde.com/docs/1"
},
{
id: "2",
url: "https://abcde.com/docs/2"
}
]
}

任何有见地的都欢迎。

编辑2021年3月10日

我能够得到一个可接受的解决方案基于Garlef Wegart下面的回应。查看这里的代码。

但是,请注意,它非常挑剔。它很适合我的用例,因为我正在输入GraphQL API,所以响应作为unknown通过网络传入,然后转换为输入参数推断的值,但这些类型在其他情况下可能不起作用。对我来说,重要的部分不是赋值部分,而是结果类型的消费,这个解决方案确实适用于此。祝其他尝试这个的人好运!第二个注意:我也把这些类型作为一个小的Typescript包发布在github上(这里)。要使用它,只需将@kael-shipman:repository=https://npm.pkg.github.com/kael-shipman添加到npmrc的某个地方,然后像往常一样安装软件包。

Projection的定义可以明显看出,keyof Projection<T>keyof T的子集——对于给定的T

但是:U延伸(!)Projection<T>,所以U本身可以有t中没有的键,这些键下存储的值基本上可以是任何值

所以:映射到keyof U是不正确的事情。相反,您可以映射到keyof T & keyof U

您还应该通过添加U extends ... & Record<string, DesiredConstraints>进一步限制U只具有您想要的属性。否则你可以传入带有不良属性的对象。(我猜在你的情况下,它应该是... & SimpleObject?)

下面是一个简化的例子(不考虑您的领域的复杂性),说明了一些复杂性(Playground链接):

type Subobject<T> = {
[k in keyof T as T[k] extends "pass" ? k : never]:
T[k]
}
type SomeGeneric<T, U extends Subobject<T>> = {
[k in keyof U]:
k extends keyof T
? "yep"
: "nope"
}
type Sub = Subobject<{ a: 1, b: "pass" }>
// This is not what we want: `c: "nope"` shoud not be part of our result!
type NotWanted = SomeGeneric<{ a: 1, b: "pass" }, { b: "pass", c: "other" }>

type Safe<T, U extends Subobject<T>> = {
[k in (keyof U & keyof T)]:
k extends keyof T
? "yep"
: "nope"
}
type Yeah = Safe<{ a: 1, b: "pass" }, { b: "pass", c: "other" }>
// Next problem: U can have bad properties!
function doStuffWithU<T, U extends Subobject<T>>(u: U) {
for (const val of Object.values(u)) {
if (val === "other") {
throw Error('Our code breaks if it receives "other"')
}
}
}
const u = { b: "pass", c: "other" } as const
// This will break but the compiler does not complain!
const error = doStuffWithU<Sub, typeof u>(u)

// But this can be prevented
type SafeAndOnlyAllowedProperties<T, U extends Subobject<T> & Record<string, "pass">> = {
[k in (keyof U & keyof T)]:
// No complaints here due to `& Record<string, "pass">`
OnlyAcceptsPass<U[k]>
}
type OnlyAcceptsPass<V extends "pass"> = "pass!"
// The type checker will now complain `"other" is not assignable to type "pass"`
type HellYeah = SafeAndOnlyAllowedProperties<{ a: 1, b: "pass" }, { b: "pass", c: "other" }>
编辑:再考虑一下:当定义函数而不是泛型类型时,您还可以使用以下模式来防止错误的输入
const safeFn = <U extends Allowed>(u: U & Constraint) => {
// ...
}

最新更新