如何使用映射类型删除索引签名

Posted

技术标签:

【中文标题】如何使用映射类型删除索引签名【英文标题】:How to remove index signature using mapped types 【发布时间】:2018-12-30 02:27:16 【问题描述】:

给定一个接口(来自无法更改的现有 .d.ts 文件):

interface Foo 
  [key: string]: any;
  bar(): void;

有没有办法使用映射类型(或其他方法)在没有索引签名的情况下派生新类型?即它只有方法bar(): void;

【问题讨论】:

哇,答案是肯定的:***.com/a/51955852/2887218 【参考方案1】:

编辑: 自 Typescript 4.1 以来,有一种方法可以直接使用 Key Remapping 执行此操作,避免使用 Pick 组合子。请参阅the answer by Oleg where it's introduced。

type RemoveIndex<T> = 
  [ K in keyof T as string extends K ? never : number extends K ? never : K ] : T[K]
;

这是基于'a' extends stringstring 不是extends 'a' 的事实。


还有一种方法可以用 TypeScript 2.8 的 Conditional Types 来表达。

interface Foo 
  [key: string]: any;
  [key: number]: any;
  bar(): void;


type KnownKeys<T> = 
  [K in keyof T]: string extends K ? never : number extends K ? never : K
 extends  [_ in keyof T]: infer U  ? U : never;


type FooWithOnlyBar = Pick<Foo, KnownKeys<Foo>>;

你可以用它做一个通用的:

// Generic !!!
type RemoveIndex<T extends Record<any,any>> = Pick<T, KnownKeys<T>>;

type FooWithOnlyBar = RemoveIndex<Foo>;

要了解KnownKeys&lt;T&gt; 的确切原因,请参阅以下答案:

https://***.com/a/51955852/2115619

【讨论】:

太棒了!谢谢!它只是表明,你永远不应该怀疑 TypeScript 的力量。 :D 下面是 GitHub 评论,似乎是这项技术的起源:github.com/Microsoft/TypeScript/issues/… 这种类型的递归版本怎么样?你认为这可能吗?比如说,以[k: string]: number, n: number, o: [k: string]: number, n: number 开头,以n: number, o: n: number 结尾。 只有一层“o”,因此您的示例不起作用是有道理的。我在初始类型上也犯了一个错误:第一个索引签名需要指向 any 而不是 number,以便允许将 o 定义为对象。考虑到这两个因素......它似乎正在工作! goo.gl/xkwG33(我会在其他时间进一步测试。) 好吧,再看一遍,其实所有类型都“迷失在翻译中”了。当悬停在B 上时,TypeScript 会说nMappedType&lt;Pick&lt;number, &gt;&gt; 类型,我想这是一种表示没有选择属性的方式(它本身实际上不是一个有效的类型)。底线是您的递归解决方案允许将任何类型分配给n,例如'abc' 将是一个有效值。 @MihailMalostanidis 这是我认为不再适用的通用版本。但我错过了extends Record&lt;any, any&gt;! (而且错误消息对我没有太大帮助。)谢谢,对不起^^'【参考方案2】:

使用 TypeScript v4.1 key remapping 带来了一个非常简洁的解决方案。

它的核心使用了来自Mihail's answer 的稍微修改的逻辑:虽然已知键是stringnumber 的子类型,但后者不是相应文字的子类型。另一方面,string 是所有可能字符串的联合(number 也是如此),因此是自反的(type res = string extends string ? true : false; //true 成立)。

这意味着每次stringnumber 类型可分配给键类型时,您都可以解析为never,从而有效地将其过滤掉:

interface Foo 
  [key: string]: any;
  [key: number]: any;
  bar(): void;


type RemoveIndex<T> = 
  [ P in keyof T as string extends P ? never : number extends P ? never : P ] : T[P]
;

type FooWithOnlyBar = RemoveIndex<Foo>; // bar: () => void; 

Playground

【讨论】:

您介意我将其内嵌到我的答案中吗(因为它被标记为已接受)? @MihailMalostanidis - 你的意思是类型?是的,当然 - 希望有一天我们会有更好的方法来解决这个问题 遗憾的是,虽然旧系统适用于具有替代品 (A | B) 的输入类型,但它不适用于 typescript 4.3,也不适用于此。 在您的示例中,联合的备选方案具有相同的成员,因此它们折叠为实际上是同一类型。我已经调整了您的示例以显示问题:tsplay.dev/NBPegW。 RemoveIndex 产生一个只接受联合成员共有的键的类型,而不是从联合成员中删除索引器的联合。 (编辑:修正了我的例子中的一个错误) @Cheetah 啊,这个。好吧,上次我检查时,这是具有类对象成员的联合的正常行为。如果您使用类型保护(根据您的示例参见playground),您将看到生成的类型不是有损的,并且所有成员信息仍然存在。但我现在明白你的意思了,直到 4.2.3 的旧版本确实做到了allow this,但这看起来像是一种意外的副作用,而不是预期的行为。【参考方案3】:

通过TypeScript 4.4,该语言获得了对更复杂索引签名的支持。

interface FancyIndices 
  [x: symbol]: number;
  [x: `data-$string`]: string

symbol 键可以通过在之前发布的类型中添加一个大小写来轻松捕获,但这种检查方式无法检测到无限的模板文字。1

但是,我们可以通过修改检查以查看使用每个键构造的对象是否可分配给空对象来实现相同的目标。这是有效的,因为“真实”键将要求使用 Record&lt;K, 1&gt; 构造的对象具有属性,因此不可分配,而作为索引签名的键将导致类型可能仅包含空对象。

type RemoveIndex<T> = 
  [K in keyof T as  extends Record<K, 1> ? never : K]: T[K]

在playground试试吧

测试:

class X 
  [x: string]: any
  [x: number]: any
  [x: symbol]: any
  [x: `head-$string`]: string
  [x: `$string-tail`]: string
  [x: `head-$string-tail`]: string
  [x: `$bigint`]: string
  [x: `embedded-$number`]: string

  normal = 123
  optional?: string


type RemoveIndex<T> = 
  [K in keyof T as  extends Record<K, 1> ? never : K]: T[K]


type Result = RemoveIndex<X>
//   ^? -  normal: number, optional?: string  

1您可以使用一次处理一个字符的递归类型来检测一些无限模板文字,但这不适用于长键。

【讨论】:

有没有一种方法可以使用像string 这样的原始类型。我尝试使用RemoveIndex&lt;string&gt;,但原始类型仍然具有字符串索引约束。 @CMCDragonkai 你到底想做什么? RemoveIndex&lt;String&gt;(注意大写 S)给你一个看起来像 string 没有索引签名的类型,但这可能不是真的有用...... 是的,我需要欺骗 TS 将类型视为原始字符串,但没有 String 具有的索引签名。【参考方案4】:

没有一个真正通用的方法,但是如果你知道你需要哪些属性,那么你可以使用 Pick:

interface Foo 
  [key: string]: any;
  bar(): void;


type FooWithOnlyBar = Pick<Foo, 'bar'>;

const abc: FooWithOnlyBar =  bar: () =>   

abc.notexisting = 5; // error

【讨论】:

不幸的是,这是针对 AngularJS IScope 接口的。它有很多属性,复制/粘贴类型定义并以这种方式修改它可能会更容易。【参考方案5】:

不是真的。你不能像这样的界面“减去”一些东西。每个成员都是公开的,任何声称实施Foo 的人都必须实施它们。通常,您只能通过extends声明合并 扩展接口,但不能从中删除内容。

【讨论】:

感谢您的回复,但我知道派生排除属性的类型当然是可能的。我一直在使用“Omit”类型,“Pick>”(在 TS 2.8 中),它允许您指定基本类型和字符串类型的联合。所以,“type Baz = Omit;”给出一个只有索引签名的新类型.我很想知道是否有类似的方法可以省略索引类型签名。 哦,我明白你在问什么。您对 兼容 的两种类型不感兴趣,只是有一种很好的方式来键入内容,而我想没有重复。

以上是关于如何使用映射类型删除索引签名的主要内容,如果未能解决你的问题,请参考以下文章

如何为映射类型添加索引签名?

编译打字稿时如何防止错误“对象类型的索引签名隐式具有'任何'类型”?

如何在 TypeScript 中添加索引签名

TypeScript:删除索引签名而不隐式出现“任何”类型错误

如何使用 JSDoc 记录符号索引签名以符合打字稿?

如何使用spring data elasticSearch映射连接类型