Typescript 类型的递归子集

2024-03-23

在 Typescript 中是否可以创建与此类似的类型子集?

type Schema = {
  user: {
    name: string;
    age: number;
    profile: {
      isCool?: boolean;
    };
  };
};

const wantedSubset = {
  user: {
    name: true,
    profile: {
      isCool: true
    }
  }
};

type ExpectedType = {
  user: {
    name: string;
    profile: {
      isCool?: boolean;
    };
  };
};

const result: ExpectedType = desiredCode(wantedSubset);

如果不清楚我想要desiredCode给定对象的函数wantedSubset将返回任何内容,但键入为递归使用declare function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K>;

我想申请这个Pick递归地Schema但我不知道如何递归地“引用”Schema type.

我尝试过这样的事情:

function gql<Q, S, QK extends keyof Q, SK extends keyof S, K extends SK & QK>(
  query: Q | true,
  schema: S
) {
  if (query === true) return (query as unknown) as S;
  const keys = Object.keys(query) as K[];
  const result = {} as Pick<S, K>;
  keys.forEach(k => {
    result[k] = gql(query[k], schema[k]);
  });

  return result;
}

const result = gql(wantedSubset, wantedSubset as unknown as Schema)

但它并没有按照我希望的方式工作。它只是返回 Pick 的结果,而不是递归地应用它。

基本上问题是如何动态构建对象,以便打字稿能够推断返回值。

https://www.typescriptlang.org/play/#src=type%20Schema%20%3D%20%7B%0D%0A%20%20user%3A%20%7B%0D%0A%20%20%20 %20名称%3A%20字符串%3B%0D%0A%20%20%20%20年龄%3A%20数字%3B%0D%0A%20%20%20%20配置文件%3A%20%7B%0D%0A%20 %20%20%20%20%20isCool%3F%3A%20boolean%3B%0D%0A%20%20%20%20%7D%3B%0D%0A%20%20%7D%2C%0D%0A %20%20foo%3A%20%7B%0D%0A%20%20%20%20bar%3A%20string%3B%0D%0A%20%20%7D%0D%0A%7D%3B%0D%0A %0D%0Aconst%20wantedSubset%20%3D%20%7B%0D%0A%20%20user%3A%20%7B%0D%0A%20%20%20%20name%3A%20true%2C%0D%0A %20%20%20%20配置文件%3A%20%7B%0D%0A%20%20%20%20%20%20isCool%3A%20true%0D%0A%20%20%20%20%7D%0D %0A%20%20%7D%0D%0A%7D%3B%0D%0A%0D%0A%0D%0Afunction%20gql%3CQ%2C%20S%2C%20QK%20extends%20keyof%20Q%2C%20SK %20extends%20keyof%20S%2C%20K%20extends%20SK%20%26%20QK%3E(%0D%0A%20%20query%3A%20Q%20%7C%20true%2C%0D%0A%20% 20架构%3A%20S%0D%0A)%20%7B%0D%0A%20%20if%20(查询%20%3D%3D%3D%20true)%20return%20(查询%20as%20未知)%20as %20S%3B%0D%0A%20%20const%20keys%20%3D%20Object.keys(查询)%20as%20K%5B%5D%3B%0D%0A%20%20const%20result%20%3D% 20%7B%7D%20as%20Pick%3CS%2C%20K%3E%3B%0D%0A%20%20keys.forEach(k%20%3D%3E%20%7B%0D%0A%20%20% 20%20结果%5Bk%5D%20%3D%20gql(查询%5Bk%5D%2C%20架构%5Bk%5D)%3B%0D%0A%20%20%7D)%3B%0D%0A%0D% 0A%20%20return%20result%3B%0D%0A%7D%0D%0A%0D%0Aconst%20result%20%3D%20gql(wantedSubset%2C%20wantedSubset%20as%20unknown%20as%20Schema)%0D%0A %0D%0Aresult.user.age%20%2F%2F%20应该%20be%20an%20错误!%0D%0Aresult.user.name%20%2F%2F%20works%20OK%0D%0Aresult.user.profile。 isCool%20%2F%2F%20works%20OK%0D%0Aresult.foo%20%2F%2F%20works%20like%20预期 https://www.typescriptlang.org/play/#src=type%20Schema%20%3D%20%7B%0D%0A%20%20user%3A%20%7B%0D%0A%20%20%20%20name%3A%20string%3B%0D%0A%20%20%20%20age%3A%20number%3B%0D%0A%20%20%20%20profile%3A%20%7B%0D%0A%20%20%20%20%20%20isCool%3F%3A%20boolean%3B%0D%0A%20%20%20%20%7D%3B%0D%0A%20%20%7D%2C%0D%0A%20%20foo%3A%20%7B%0D%0A%20%20%20%20bar%3A%20string%3B%0D%0A%20%20%7D%0D%0A%7D%3B%0D%0A%0D%0Aconst%20wantedSubset%20%3D%20%7B%0D%0A%20%20user%3A%20%7B%0D%0A%20%20%20%20name%3A%20true%2C%0D%0A%20%20%20%20profile%3A%20%7B%0D%0A%20%20%20%20%20%20isCool%3A%20true%0D%0A%20%20%20%20%7D%0D%0A%20%20%7D%0D%0A%7D%3B%0D%0A%0D%0A%0D%0Afunction%20gql%3CQ%2C%20S%2C%20QK%20extends%20keyof%20Q%2C%20SK%20extends%20keyof%20S%2C%20K%20extends%20SK%20%26%20QK%3E(%0D%0A%20%20query%3A%20Q%20%7C%20true%2C%0D%0A%20%20schema%3A%20S%0D%0A)%20%7B%0D%0A%20%20if%20(query%20%3D%3D%3D%20true)%20return%20(query%20as%20unknown)%20as%20S%3B%0D%0A%20%20const%20keys%20%3D%20Object.keys(query)%20as%20K%5B%5D%3B%0D%0A%20%20const%20result%20%3D%20%7B%7D%20as%20Pick%3CS%2C%20K%3E%3B%0D%0A%20%20keys.forEach(k%20%3D%3E%20%7B%0D%0A%20%20%20%20result%5Bk%5D%20%3D%20gql(query%5Bk%5D%2C%20schema%5Bk%5D)%3B%0D%0A%20%20%7D)%3B%0D%0A%0D%0A%20%20return%20result%3B%0D%0A%7D%0D%0A%0D%0Aconst%20result%20%3D%20gql(wantedSubset%2C%20wantedSubset%20as%20unknown%20as%20Schema)%0D%0A%0D%0Aresult.user.age%20%2F%2F%20should%20be%20an%20error!%0D%0Aresult.user.name%20%2F%2F%20works%20OK%0D%0Aresult.user.profile.isCool%20%2F%2F%20works%20OK%0D%0Aresult.foo%20%2F%2F%20works%20like%20expected


所以我想你想要wantedSubset符合每个属性要么是的类型true或者它本身就是该类型的对象。我们需要做一些工作才能让 TypeScript 推断出值的类型true被视为类型true并不是boolean(至少在 TS3.4 发布之前给我们带来const语境 https://github.com/Microsoft/TypeScript/pull/29510):

type WantedSubset = { [k: string]: true | WantedSubset };
const asWantedSubset = <RP extends WantedSubset>(wantedSubset: RP) => wantedSubset;

const wantedSubset = asWantedSubset({
  user: {
    name: true,
    profile: {
      isCool: true
    }
  }
}); // no error, so that type works.

现在来了类型RecursivePick和朋友。首先,给定一个类型T, 你想要一个WantedSubset符合它的类型。我称之为RecusivePicker<T>:

type RecursivePicker<T> = { 
  [K in keyof T]?: T[K] extends object | undefined ? RecursivePicker<T[K]> : true 
}

So if T is Schema, then RecursivePicker<Schema>给你:

type RPSchema = RecursivePicker<Schema>
// type RPSchema = {
//  user?: {
//    age?: true;
//    name?: true;
//    profile?: {
//      isCool?: true;
//    }
//  }
// } 

这可能是对类型的限制wantedSubset你可以选择Schema types.

这是RecursivePick在所有递归映射条件 horror 的荣耀中:

type RecursivePick<T, KO extends RecursivePicker<T>> =
  Pick<{ [K in keyof T]: K extends keyof KO ?
    KO[K] extends true ? T[K] :
    KO[K] extends infer KOK ?
    KOK extends RecursivePicker<T[K]> ? RecursivePick<T[K], KOK> :
    never : never : never }, keyof T & keyof KO>;

它基本上遍历了以下属性T并检查相应的属性KO。如果该房产是true它返回的属性T不变。如果属性本身就是一袋属性,那么它会向下递归。如果该属性不存在,则会返回never。整件事就是Picked,以便只有键同时出现在两者中T and KO出现在最终输出中(这有点麻烦,以确保所有相关的映射类型 https://www.typescriptlang.org/docs/handbook/advanced-types.html#mapped-types是同态的,意味着可选属性保持可选)。

让我们验证一下它是否有效:

type ExpectedType = RecursivePick<Schema, typeof wantedSubset>;

您可以完成这个过程,但让编译器验证它:

type ExpectedTypeManual = {
  user: {
    name: string;
    profile: {
      isCool?: boolean;
    };
  };
};

type MutuallyExtends<T extends U, U extends V, V=T> = true

// if no error in the next line, then ExpectedType and ExpectedTypeManual are 
//  structurally the same
type NoErrorHere = MutuallyExtends<ExpectedType, ExpectedTypeManual>

这样一切都有效。您的函数将键入如下内容:

declare function gql<Q extends RecursivePicker<S>, S>(
  query: Q | true,
  schema: S
): RecursivePick<S, Q>;

const result = gql(wantedSubset, null! as Schema); // looks good

使函数的实现编译无错误可能需要overload https://www.typescriptlang.org/docs/handbook/functions.html#overloads因为条件类型很难推断:

function gql<Q extends RecursivePicker<S>, S>(
  query: Q | true,
  schema: S
): RecursivePick<S, Q>;
function gql(query: any, schema: object): object {
  return {}; // something better here
}

好吧,希望有帮助;祝你好运!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Typescript 类型的递归子集 的相关文章

随机推荐

  • 使用 PHP 和 MySQL 创建多维数组

    我是 PHP 新手 正在寻找从数据库返回数据的有效方法 假设我有一个 UserProfile 表 它与 UserInterest 和 UserContact 具有一对多关系 Select p Id p FirstName p LastNam
  • ItemsControl 和 Canvas 中的多个数据模板

    我试图在画布上显示一些框 我自己的 userControl 在其自己的名为 singleNodeControl 的 xaml 文件中定义 并用线连接它们 普通 xaml Line 元素绑定到 LineToParent 类 这两项都存储在 v
  • 使用 PHPExcel 读取电子表格

    我正在尝试上传电子表格并使用 PHPExcel 将其读入 MySQL 数据库 对于 xlsx 文件 它工作正常 但每当我尝试上传 ods 文件时 它都会抛出错误 PHP Fatal error Call to a member functi
  • 如何使用 C# 更新数据透视表数据源?

    我想知道如何更新现有的数据透视表数据源 我在用Microsoft Office Interop Excel并针对使用 Excel 2010 的用户 我目前能够刷新工作正常的数据透视表 但是当添加更多行时 我希望将这些行包含在数据透视表数据源
  • WPF:我可以强制窗口重新评估其所有绑定和验证吗?

    我可以强制窗口重新评估其所有绑定和验证吗 由于某种原因 它似乎在一种奇怪的情况下忽略了 INotifyPropertyChanged PropertyChanged 我正在寻找一种解决方法 直到找到真正的原因 不幸的是 我知道没有办法强制窗
  • 如何在Linux中安装chrome(无头)

    我有一个运行 linux redhad 的 AWS EC2 有没有办法在上面安装最新的 Chrome v59 以便我可以像 PhantomJS 一样以无头模式运行它 我在 google 上能找到的所有资源都是关于如何在有 UI 的 ubun
  • 无法转换“UICollectionViewCell”类型的值

    我在 Storyboard 中使用自定义 CollectionViewCell 当我启动应用程序时 我收到以下消息 无法将 UICollectionViewCell 类型的值转换为 TestProject CollectionViewCel
  • 框架在不同时间绘画? [关闭]

    Closed 这个问题需要调试细节 help minimal reproducible example 目前不接受答案 我的游戏中有一个非常烦人的错误 帧的底部似乎比帧的顶部渲染得更早 我不确定为什么会发生这种情况 我正在使用 JPanel
  • Python 的 bool 值是按值传递的吗?

    我发送了对 bool 对象的引用 并在方法中修改了它 方法执行完毕后 方法外的bool值没有变化 这让我相信 Python 的 bool 是按值传递的 真的吗 还有哪些其他 Python 类型有这样的行为 Python 变量不是 C 意义上
  • Pip 安装日志在哪里?

    为什么 pip 不记录何时安装了哪个版本的库 如果您将库更新为损坏的版本怎么办 你怎么知道哪个版本没有被破坏 那些对此投赞成票的人 你能告诉我你为什么这样做吗 运行 pip 时 您可以指定日志文件 这样您就可以在将来跟踪安装日志 pip i
  • n最大和n最小;堆Python

    这是出于对 python 中 heapq py 模块的 nsmallest 和 nlargest 方法的好奇 我正在读它here https docs python org 2 library heapq html 在文档中 文档没有说明它
  • 按类型组合连续日期时间间隔

    假设我们有这样一个表 declare periods table s date e date t tinyint 日期间隔无间隙 按开始日期排序 insert into periods values 2013 01 01 2013 01 0
  • 如何在 Typescript 中启用 NodeJS 和 ExpressJs

    我希望找到一个适用于 Node 和 Express 的 d ts 文件 这将在打字稿中启用 Intellisense 到目前为止 我已经尝试使用此处提供的 Node d ts 文件 https github com borisyankov
  • Maven 快照到底是什么以及我们为什么需要它?

    我对 Maven 快照的含义以及我们为什么要构建一个有点困惑 Maven 中的快照版本是尚未发布的版本 这个想法是before a 1 0发布 或任何其他发布 完成后 存在1 0 SNAPSHOT 那个版本是什么可能会成为 1 0 基本上就
  • 一台服务器向 Android 和 iOS 设备发送推送通知

    我们的组织有一个 Android 应用程序和一个 iOS 应用程序 我们希望开始向这些应用程序推送通知 安卓有GCM 苹果有APNS 但我们想要创建一个可以在 Android 和 iOS 上运行的 API 设置服务器的最简单方法是什么 以便
  • 反应式香蕉中的动态事件切换导致严重泄漏

    我不确定这种行为是预期的 即我误用了 Reactive Banana Switch 还是错误 假设我有两个类似类型的输入行为 并且我想根据事件在它们之间进行切换 我写了这个函数 switchBehaviors Behavior t a Be
  • 从 WCF 服务抛出FaultException 会导致“此故障的创建者未指定原因”。

    当投掷一个FaultException
  • 从 openapi-generator 生成提示登录时添加行 import openapiclient "github.com/GIT_USER_ID/GIT_REPO_ID"

    我正在使用 openapi 生成器来生成我的其余 api 客户端 它生成行 openapiclient github com GIT USER ID GIT REPO ID 在我的进口中 但我一生都无法理解为什么 运行一个go mod ve
  • CONN_MAX_AGE 在 Django 中如何工作

    有人可以 ELI5 CONN MAX AGE 做什么吗 我认为它是这样工作的 1 请求 1 进入 打开到数据库的连接 1 2 请求 1 使用连接 1 来做一些工作 3 请求 1 完成 由于 CONN MAX AGE 不为零 并且尚未达到期限
  • Typescript 类型的递归子集

    在 Typescript 中是否可以创建与此类似的类型子集 type Schema user name string age number profile isCool boolean const wantedSubset user nam