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

Typescript Cannot index type T using key defined as subset of keys of type T

提问人:kael 提问时间:2/24/2021 最后编辑:kael 更新时间:3/13/2021 访问量:495

问:

注意:请参阅下面的编辑,了解基于 @GarlefWegart 响应的最终解决方案。

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

我非常接近,但发现了一个奇怪的问题。完整的代码在操场上,并在下面复制。

问题在于使用派生对象的键对对象进行索引,这应该有效,但由于某种原因失败了。请注意,在定义中,我不能使用 进行索引,即使它被定义为 的键,并且被定义为 的属性的子集。这意味着 的所有键也必然是 的键,因此使用任何键 进行索引应该是安全的。但是,Typescript 拒绝这样做。ResultTKKUUTUTTU

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: "[email protected]",
    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

第二点:我还在 github 上将这些类型作为小型 Typescript 包发布(这里)。要使用它,只需将 @kael-shipman:repository=https://npm.pkg.github.com/kael-shipman 添加到您的 npmrc 的某处,然后像往常一样安装包。

打字稿 TypeScript 打字

评论

0赞 Gerrit Begher 2/25/2021
它不应该在定义中吗?Array<Projection<A>>Projection
1赞 kael 2/26/2021
@GarlefWegart这是 GraphQL 语法中的一个奇怪的怪癖。GraphQL 忽略了数组,而是只关注对象的形状。我这样编写它,以便您可以编写看起来就像 GrpahQL 语法一样的投影。

答:

3赞 Gerrit Begher 2/26/2021 #1

从定义来看,这是真实而明显的,它是 -- 的子集。Projectionkeyof Projection<T>keyof TT

但是:扩展(! 所以它本身可以有 T 中不存在的键。存储在这些键下的值基本上可以是任何东西。UProjection<T>U

所以:映射不是正确的做法。相反,您可以映射 .keyof Ukeyof T & keyof U

您还应该通过添加 .否则,您可以传入具有错误属性的对象。(我想在你的情况下应该是?UU extends ... & Record<string, DesiredConstraints>... & 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) => {
  // ...
}

评论

0赞 kael 2/27/2021
啊啊,“扩展,因此可以具有其他属性”是我没有想到:)的好点。你知道吗,在这种情况下,有什么方法可以在不扩展的情况下别名?我对“默认值”语法(即 )的理解是 - 它提供了一个默认值,但最终用户可以分配一个不同的值。UProjection<T>Projection<T>U = Projection<T>
1赞 kael 3/11/2021
好的,在你的回答的帮助下,我能够得到我想要的东西,所以我接受了它。我的最终解决方案与我上面的原始问题有联系,尽管应该注意的是,它非常挑剔。(我无法在评论中包含链接,因为它太长了。感谢您朝着正确的方向推动!