【问题标题】:Can TypeScript iterate over an interface's "own" keys only?TypeScript 可以只迭代接口的“自己的”键吗?
【发布时间】:2021-03-09 18:34:03
【问题描述】:

有什么方法可以构建一个类似于DeepRequired<T> 的帮助器类型,它只会影响不是T“外部”的键?

我的意思是,用这个示例代码:

interface Test{
  prop1?: string
  prop2: {
    prop3?: {
      prop3b?: HTMLElement
    }
    prop4?: number
  }
}

declare const test: DeepRequired<Test>
const c: HTMLElement = test.prop2.prop3.prop3b

这将在c 上失败并出现错误:

键入'{ accessKey:字符串;只读 accessKeyLabel: string;自动大写:字符串;目录:字符串;可拖动:布尔值;隐藏:布尔值;内部文本:字符串;语言:字符串;只读 offsetHeight: number; ... 233 更多 ...;焦点:(选项?:FocusOptions | undefined)=> void; }' 不可分配给类型 'HTMLElement'。

如果我理解正确,那是因为 prop3b?: HTMLElement 已转换为 prop3b: DeepRequired&lt;HTMLElement&gt;

有什么办法可以“深而浅”?

我尝试过构建自己的递归类型,比如:

type IsObject<T> = T extends AnyArray
  ? false
  : T extends object
  ? true
  : false

type RRequired<T> = {
  [P in keyof T]-?: NonNullable<T[P]> extends Builtin
    ? T[P]
    : NonNullable<T[P]> extends AnyArray
    ? T[P]
    : IsObject<NonNullable<T[P]>> extends true
    ? RRequired<T[P]>
    : T[P]
}

...但我找不到停止递归的方法。

我的理解是,对于TypeScript,我的Test接口相当于:

interface Test{
  prop1?: string
  prop2: {
    prop3?: {
      prop3b?: {
        ...HTMLElement
      }
    }
    prop4?: number
  }
}

即所有HTMLElement 的属性(或任何其他类型/接口)都内联在我使用它们的类型中......所以我不知道如何区分我的接口的嵌套属性和“外部”的嵌套属性,非-内置类型。

那么,有没有办法编写一个RRequired&lt;T&gt; 类型,将Test 转换为以下类型?

RRequired<Test> == {
  prop1: string
  prop2: {
    prop3: {
      prop3b: HTMLElement // <- preserve HTMLElement
    }
    prop4: number
  }
}

【问题讨论】:

    标签: typescript typescript-generics


    【解决方案1】:

    实现这样的事情的唯一方法是检查您不希望深入要求的“允许类型”列表。例如

    type DeepRequired<T> = T extends HTMLElement ? T : {
        [K in keyof T]-?: DeepRequired<T[K]>
    }
    

    type AllowedTypes = HTMLElement | WhateverTypeYouWantToAllow | ...
    type DeepRequired<T> = T extends AllowedTypes ? T : {
        [K in keyof T]-?: DeepRequired<T[K]>
    }
    

    但我们知道这会检查结构是否匹配。这意味着如果您有一个形状与HTMLElement 完全相同的类型,或者您的AllowedTypes 中的任何元素具有相似的形状,则条件将短路并且不会给您想要的结果。这里Bogus 的类型与Test.prop2 中的所有内容相似,这意味着prop2 以下的所有内容都不需要

    type Bogus = {
        prop4?: number;
    }
    
    type AllowedTypes = HTMLElement | Bogus;
    
    type DeepRequired<T> = T extends AllowedTypes ? T : {
        [K in keyof T]-?: DeepRequired<T[K]>
    }
    
    declare const z: DeepRequired<Test>
    z.prop2.prop3.prop3b // BOOM, prop3 is optional!
    

    由于 HTMLElement 和它的所有子类型都是非常复杂的形状,你很可能会用这种方法走得很远。但在结构化类型系统中,形状就是一切,随之而来的是一些陷阱。

    【讨论】:

    • 谢谢@ddprrt!我“害怕”它可能会出现这种情况......我之前在玩弄一个想法:拥有一个我可以使用的Preserve&lt;T&gt; 类型,比如Preserve&lt;HTMLElement&gt;,来标记我不想递归到的类型。有没有办法将AllowedTypes 的支票与Preserve 的支票交换?会T extends Preserve&lt;infer P&gt; ? P : ... 吗?试试看
    • 哦,我猜你的意思是z.prop2.prop4 是可选的?
    • 不,甚至 z.prop2.prop3 也是可选的,因为 Bogus 扩展了 Test.prop2。还是亚种!有一种方法可以实现 Preserve 类型:阅读 TypeScript 中的 Opaque 类型技术:codemix.com/opaque-types-in-javascript——然而,它在某些时候会涉及到令人讨厌的类型转换,您将无法绕过。
    • 是的,这确实是我在 ts-essential 中看到的不透明类型,它给了我这个想法。你对类型转换是什么意思?我设法做到了这一点,通过类型推断,我可以“解决”Preserve&lt;T&gt;T:tsplay.dev/zwO96m – 有什么我遗漏的东西会在路上咬我吗?
    • 只是需要注意的一点:如果您要构造Test 类型的对象,则必须将您在prop3b 传递的元素转换为Preserved&lt;HTMLElement> (或其子类型之一。例如:tsplay.dev/PmLPZm
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-01-22
    • 2021-12-06
    • 2014-08-07
    • 2016-12-18
    • 2023-02-21
    • 2020-11-07
    • 1970-01-01
    相关资源
    最近更新 更多