首页 > 解决方案 > 在具有索引类型签名的类上使用省略类型会导致不需要最少的属性

问题描述

我有一个类,它具有一些强制性属性,然后可以具有由扩展该属性的类定义的其他属性(或者无论如何,一些我不知道的额外属性)。

现在我正在尝试Omit在其上使用类型,定义为

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

如果我们将类定义为

class Thing {
  constructor(
     public id: number,
     public name: string,
     public description: string
  ) {}
}

我们可以做Omit<Thing, 'id'>并得到正确的类型推断Pick<Thing, "name" | "description">

如果我们[index: string]: any在类中添加一个以支持未知属性并重试Omit<Thing, 'id'>,现在推断的类型将是Pick<Thing, string | number>.

class Thing {
  constructor(
     public id: number,
     public name: string,
     public description: string
  ) {}

  [index: string]: any;
}

这并没有真正的意义:当我使用类键入时,即使[index: string]: any;定义了 a 也需要所有属性,为什么在省略其中一个属性时它会变成更宽松的类型?我希望返回类型仍然是Pick<Thing, "name" | "description">.

有谁知道发生了什么?

标签: typescript

解决方案


打字稿 4.1+ 的更新

由于 typeScript 4.1在映射类型中引入了键重映射,您现在可以编写一个不难看的版本,Omit它可以更优雅地处理具有索引签名的类型:

type NewOmit<T, K extends PropertyKey> = 
  { [P in keyof T as Exclude<P, K>]: T[P] };

您可以验证它是否按需要工作:

type RemoveId = NewOmit<Thing, "id">;
/* type RemoveId = {
    [x: string]: any;
    name: string;
    description: string;
} */

好的!

Playground 代码链接


预打字稿 4.1 答案

你可以制作一个Omit<>处理索引签名的,但它真的很难看。

如前所述,keyof T如果T有一个字符串索引签名是string | number,并且任何文字键(如'id')都会被吃掉。可以通过一些疯狂的类型系统箍跳来获得具有索引​​签名的类型的文字键。一旦你有了文字键和索引签名键,你就可以建立一个Omit

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

type Lookup<T, K> = K extends keyof T ? T[K] : never;
type _IndexKey<T> = string extends keyof T ?
  Lookup<T, string> extends Lookup<T, number> ?
  string : (string | number) : number extends keyof T ? number : never;
type IndexKey<T> = _IndexKey<T> extends infer K ?
  [K] extends [keyof T] ? K : never : never;

type WidenStringIndex<T extends keyof any> =
  string extends T ? (string | number) : T;

type Omit<T, K extends keyof T,
  O = Pick<T, Exclude<
    LiteralKeys<T>, K>> & Pick<T, Exclude<IndexKey<T>, WidenStringIndex<K>>>
  > = { [P in keyof O]: O[P] }

我说丑。

您可以验证它的行为是否符合您的预期:

type RemoveId = Omit<Thing, 'id'>;
// type RemoveId = {
//  [x: string]: any;
//  name: string;
//  description: string;
// }

如果您想使用上述Omit内容,请将其放入某个无需任何人查看的库中,然后在您的代码中使用它;这取决于你。我不知道它处理所有可能的边缘情况,甚至在某些情况下“正确”的行为是什么(例如,你可以同时拥有 astring和 a值比值窄的number索引......你怎么办?想看看你是否属于这种类型?‍♂️)所以请自行承担风险。但我只想指出,某种解决方案可能的。numberstringOmit<T, string>

好的,希望有帮助。祝你好运!


推荐阅读