扩展并仅指定已知属性?

2023-12-04

我试图提供一个接受给定类型的映射的接口,并将其用于运行时逻辑和编译时类型。

就像是:

type SomeType = {
  a: string
  b: { a: string, b: string }
}
magicalFunction({ a: 1 }) // 1. return type is {a: string}
magicalFunction({ b: 1 }) // 2. return type is { b: { a: string, b: string } }
magicalFunction({ b: { a: 1 } }) // 3. return type is { b: { a: string } }
magicalFunction({ a: 1, c: 1 }) // 4. compile-time error since there's no 'c' on SomeType

(在现实世界,magicalFunction takes SomeType作为通用参数。对于这个问题,我们可以假设它只是硬编码的SomeType.)

使用映射类型时,我得到了前 3 个行为:

export type ProjectionMap<T> = {
  [k in keyof T]?: T[k] extends object
    ? 1 | ProjectionMap<T[k]>
    : 1

export type Projection<T, P extends ProjectionMap<T>> = {
  [k in keyof T & keyof P]: P[k] extends object
    ? Projection<T[k], P[k]>
    : T[k]
}

type SomeType = {
  a: string
  b: { a: string, b: string }
}

function magicalFunction<P extends ProjectionMap<SomeType>>(p: P): Projection<SomeType, P> {
  /* using `p` to do some logic and construct something that matches `P` */
  throw new Error("WIP")
}

const res = magicalFunction({ a:1 })
// etc

问题是我在指定额外属性时没有收到编译错误 - 例如{a:1, c:1}。现在,它实际上非常有意义 - 在这种情况下,编译器推断P as typeof { a:1, c:1 },这是结果的有效子类ProjectionMap<SomeType>因为所有字段都是可选的。但是是否有一些神奇的咒语可以使这项工作如我所描述的那样进行呢?指定类型的不同方式,或者一些映射类型的魔法?

据我所知,操纵P当指定类型时p参数破坏类型推断。如果我们假设存在密钥过滤类型(未知密钥失败,递归),则将签名更改为magicalFunction<...>(p: FilterKeys<P, SomeType>): ... 拨打电话,例如magicalFunction({a:1})决心magicalFunction<unknown>.

背景

我的最终目标是创建一个存储库类,该类被输入到特定实体,并且可以针对 mongo 执行投影查询。为了类型安全,我希望获得投影字段的自动完成、指定不存在字段时的编译错误以及与投影匹配的返回类型。

例如:

class TestClass { num: number, str: string }
const repo = new Repo<TestClass>()
await repo.find(/*query*/, { projection: { num: 1 } }) 
// compile-time type: { num: number}
// runtime instance: { num: <some_concrete_value> }

您可能知道,TypeScript 中的对象类型通过以下方式进行匹配结构子类型因此open and 可扩展的. If Base是一个对象类型并且Sub extends Base,那么一个Sub是一种Base,并且您应该被允许使用Sub无论何处Base是必须的:

const sub: Sub = thing; 
const base: Base = sub; // okay because every Sub is also a Base

这意味着每个已知的属性Base必须是已知的属性Sub。但反过来并不意味着:并不是每个已知的属性Sub必须是已知的属性Base。事实上,您可以轻松地将新的已知属性添加到Sub在不违反结构子类型的情况下:

interface Base {
    baseProp: string;
}
interface Sub extends Base {
    subProp: number;
}
const thing = { baseProp: "", subProp: 123 };

A Sub仍然是一种Base即使它有额外的属性。所以Base的属性不能仅限于那些known由编译器。

因此,TypeScript 中的对象类型是开放且可扩展的,而不是closed或“精确”。目前 TypeScript 中没有以下形式的特定类型Exact<Sub>它只允许已知的属性Sub存在并拒绝任何具有额外属性的东西。有一个长期开放的请求:微软/TypeScript#12936支持这种“精确类型”,但这不是该语言目前所具备的。

让事情变得复杂的是,对象字面量确实经历所谓的“超额财产检查”,其中意外的属性会产生编译器警告。但这更像是一个 linter 规则,而不是类型系统功能。即使以下内容会产生编译器警告:

const excessProp: Base =
    { baseProp: "", subProp: 123 }; // error!
// ---------------> ~~~~~~~~~~~~
// Object literal may only specify known properties,
// and 'subProp' does not exist in type 'Base'

这并不意味着分配给的值excessProp是无效的Base。您仍然可以这样做:

const excessProp = { baseProp: "", subProp: 123 };
const stillOkay: Base = excessProp; // no error

因此,从某种意义上说,确实没有办法在所有可能的情况下强制执行您正在寻找的约束。无论我们提出什么解决方案,总有人可以做到这一点:

const x = { a: 1, c: 1 } as const; // okay
const y: { a: 1 } = x; // okay
magicalFunction(y); // okay

这可能不太可能,您不想担心实施magicalFunction()防御它,但要记住这一点。额外的属性不会违反 TypeScript 对象类型,因此额外的属性可能会潜入。


无论如何,虽然目前还没有办法说像这样的特定类型ProjectionMap<SomeType>不能包含多余的属性,可以使其成为自引用通用约束形式的P extends ProjectionMap<SomeType, P>。你可以想到P作为要检查的候选类型。如果P某处有多余的属性,你可以使ProjectionMap<SomeType, P>与它不兼容。

这基本上是模拟精确类型的解决方法。而不是具体的Exact<T>,我们有通用的X extends Exactly<T, X>当且仅当X可分配给T但没有多余的属性。看这条评论有关详细信息,请访问 microsoft/TypeScript#12936。

这里是:

type Exactly<T, X> = T & Record<Exclude<keyof X, keyof T>, never>

所以类型Exactly<T, X> uses the Exclude<T, U>实用型采取键列表 from X哪些不存在于T...也就是多余的钥匙。它用the Record<K, V>实用型创建一个对象类型,其键是这些多余的键,其值是the never type,JavaScript 中无法为其分配任何值。例如,Exactly<{a: string}, {a: string, b: number}>将相当于{a: string, b: never}。有了那个never那里有什么使X extends Exactly<T, X>失败时X有钥匙不在T.

现在我们可以使用Exactly递归地内部ProjectionMap定义(即使在嵌套对象类型中也禁止多余的键):

type ProjectionMap<T, P extends ProjectionMap<T, P> = T> = Exactly<{
    [K in keyof T]?: T[K] extends object
    ? 1 | ProjectionMap<T[K], P[K]>
    : 1 }, P>

进而Projection没有改变:

type Projection<T, P extends ProjectionMap<T>> = {
    [K in keyof T & keyof P]: P[K] extends object
    ? Projection<T[K], P[K]> : T[K]        
} extends infer O ? { [K in keyof O]: O[K] } : never

And magicalFunction限制P to ProjectionMap<SomeType, P>:

type SomeType = {
    a: string
    b: { a: string, b: string }
}

declare function magicalFunction<
  P extends ProjectionMap<SomeType, P>
>(p: P): Projection<SomeType, P>

我们来测试一下:

magicalFunction({ a: 1 }) // {a: string}
magicalFunction({ b: 1 }) // { b: { a: string, b: string } }
magicalFunction({ b: { a: 1 } }) // { b: { a: string } }
magicalFunction({ a: 1, c: 1 }) // error, no 'c' on SomeType
magicalFunction({ b: { a: 1, z: 1 } }) // error, no 'z' on SomeType["b"]

看起来不错!您想受理的案件全部受理,您想驳回的案件全部驳回。当然magicalFunction(y)之前的问题并没有消失,但是这个实现可能足以满足您需要支持的用例。

Playground 代码链接

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

扩展并仅指定已知属性? 的相关文章

随机推荐

  • 我可以检测 iOS 的“深色”设置吗?

    我注意到 当启用 深色 iOS 系统设置时 我的应用程序看起来不太好 有些导航按钮是白色的 有些则变暗为灰色 有什么方法可以检测此设置是否启用 事实证明它很容易被发现 假设navigationBar配置为白色tintColor 刚刚读书na
  • Python和sqlite3:删除多行[重复]

    这个问题在这里已经有答案了 我需要使用 SQL 语句从 sqlite3 表中删除多行 例如 DELETE FROM table WHERE id IN 23 19 35 16 12 78 我的问题是用 Python 进行编码 并将 ids
  • 赋予 元素自动对焦

    使用以下代码 我尝试在加载页面时给予 a 元素自动聚焦 a href setting class button big active Next vocabulary a 虽然这适用于输入字段 但不适用于该元素 有人能给我解释一下吗 Rega
  • 使用 jquery 悬停地图区域精灵

    我正在开发我的第一个实际的 WordPress 网站 但我在使用 jquery 时遇到了一些问题 我真的希望有人能在这里帮助我 所以我想做的是根据当前悬停的地图区域来定位我的 div 背景 我在这里找到了一个例子 http ubytujna
  • Dal(带有实体框架)和模型层进入 MVC

    首先 我使用EF进入Dal层 与 MVC 分离的项目 相同的解决方案 从 EF 的 EDMX 文件生成的模型是来自的实际模型Model层 如果是这样 我如何访问这些模型以在 MVC 中工作View层 我认为直接从视图访问数据层来使用这些模型
  • 从现有数据库生成 JPA 2 实体

    如何从现有数据库生成符合 JPA2 标准的 Entity 我找到了这个 Question 仍然不清楚 JBoss 是否会生成兼容的 JPA2 而且我也想知道是否有一种独立于供应商的方法来做到这一点 您可以使用类似的插件日食大理为你做这件事
  • ViewPager 中的片段在旋转时不会在其 RecyclerView 中显示任何内容

    我在与 TabLayout 集成的 ViewPager 中有 4 个片段 这些片段中的每一个都包含一个 RecyclerView 因为我正在显示未知数量的列表项 这些项目是按日期加载的 因此我有两个按钮可以让您更改日期 然后根据设置的日期加
  • 在 JSF 组件中使用 render 是否足以安全地防止伪造的操作调用?

    我们一直使用自定义 Secure 拦截器来保护我们的支持 bean 方法 以防止伪造该方法的调用 但最近 我突然意识到 如果调用操作的组件未呈现 则这些方法将无法访问 据我了解 JSF 将生成视图 并且如果组件不是根据权限呈现的 例如带有
  • Azure 事件中心是否保证至少一次传递?

    我正在构建一个 Azure Web 应用程序 我想将活动日志发送到 Azure 事件中心 如果应用程序主机和事件中心之间的连接丢失会发生什么 事件中心客户端是否实现某种本地队列 TLDR 是的 EventHubs 提供至少一次交付 Even
  • ASP.NET:自定义动态填充站点地图(SiteMap Provider)

    我正在尝试写我自己的第一个SiteMapProvider子类 它旨在使用一堆不同的数据库查找动态填充 就像我在网上找到的所有示例一样 然而 有很多事情我不太清楚 这是我的第一个问题 Why is StaticSiteMapProvider几
  • 函数内部由 ddply 中指定的变量组成的子集

    通常 我需要通过对应用 ddply 的另一个 data frame 进行子集化的变量来对函数内的 data frame 进行子集化 为此 我再次显式地在函数内写入变量 我想知道是否有更优雅的方法来做到这一点 下面我举了一个简单的例子 只是为
  • 如何使用 matlab 从 yuv 420 视频剪辑中提取帧并将它们存储为不同的图像?

    如何从 yuv 420 视频中提取帧 假设我想将它们存储为静态图像 如何 这是来自MathWorks 文件交换那应该做你想做的事 将 YUV CIF 4 2 0 视频文件转换为图像文件 by Da Yu 功能loadFileYuv从上面的提
  • 中的每个文件创建 FormData 实例

    我正在尝试使用输入文件multiple属性并拆分 formData 这样我就可以为多文件元素中的每个文件提供一个 formData HTML
  • 使用 ITextSharp 创建可填写的 PDF 表单

    我使用 Acrobat Adob e 8 创建了一个包含可填写字段的 PDF 文件 可以使用 ITextSharp 而不是 Adob e 进行编程吗 注意 我并不是指使用 ITextSharp 填写 PDF 而是我想create使用 ITe
  • 将时区添加到 data.table 中的 POSIXct 对象

    我有一个 data table 对象 其中日期和时间列存储为 IDate ITime 对象 我还有一个时区列 其中时区以字符形式给出 现在我想创建一个使用 POSIXct 格式的 DateTime 列 但是我不知道如何向对象添加正确的时区
  • 有没有办法判断 python 是否使用“--with-threads --enable-shared”进行配置和编译?

    这是针对 Debian Squeeez 上的 Python 2 6 6 我试图查明 debian 附带的二进制文件是否配置了以下标志 with threads enable shared 就好像它们不是一样 我需要自己从源代码编译和安装 w
  • Solidity 中的动态数组

    我想声明一个简单的数组 动态列表 一个set函数推入一个字符串和一个get函数返回动态数组中保存的所有字符串 我搜索了很多但找不到这个简单的东西 这是我的解决方案 你需要experimental ABIEncoderV2返回字符串数组 pr
  • 如何在 DASH 内容的

    我已使用以下方式在网页中嵌入了 DASH 视频
  • ID/类别选择器

    我对 jQuery 选择器有一些问题 假设我想选择 elementID 但是elementID是一个变量 除了以其他方式执行此操作之外 还有其他可能性var variable elementID variable 我的意思是没有指定 还有其
  • 扩展并仅指定已知属性?

    我试图提供一个接受给定类型的映射的接口 并将其用于运行时逻辑和编译时类型 就像是 type SomeType a string b a string b string magicalFunction a 1 1 return type is