TypeScript:将通用接口作为其他接口的并集

时间:2019-05-24 16:36:11

标签: node.js typescript generics interface

我想创建一个通用接口,其属性表示来自其他接口的属性的并集。

假设我有两个界面

interface A {
    something: string;
    somethingElse: number;
}

interface B {
    something: Array<string>;
}

我不想将接口C编写为

interface C {
    something: string | Array<string>;
    somethingElse?: number;
}

因为这意味着每当我修改接口AB中的任何一个时,我也需要手动修改接口C

根据我在TypeScript文档中看到的内容以及在Stack Overflow上的答案,我应该声明一个新类型

type unionOfKeys = keyof A | keyof B;

并实现通用接口形式

interface GenericInterface {
    <T>(arg: T): T;
}

我在朝着

的方向思考
interface C {
    <T extends unionOfKeys>(arg: T): T extends unionOfKeys ? A[T] | B[T] : any
}

但是失败了,因为许多属性与其类型之间不匹配。

我将不胜感激。谢谢。

3 个答案:

答案 0 :(得分:2)

我认为以下MergeUnion<T>版本可能会按照您的意愿进行操作:

type MergeUnion<T> = (
  keyof T extends infer K ? [K] extends [keyof T] ? Pick<T, K> & {
    [P in Exclude<(T extends any ? keyof T : never), K>]?:
    T extends Partial<Record<P, infer V>> ? V : never
  } : never : never
) extends infer U ? { [K in keyof U]: U[K] } : never;

type C = MergeUnion<A | B>;
// type C = { 
//  something: string | string[]; 
//  somethingElse?: number | undefined; }
// }

这与其他答案类似,因为它找到T的所有成分的所有键的并集(称为UnionKeys,定义为T extends any ? keyof T : never)并返回一个映射输入所有内容。区别在于,这里我们还找到T的所有成分的所有键的交集(称其为IntersectKeys,仅定义为keyof T),并分割了键T分为两组密钥。交集中的每个元素都存在于每个组成部分中,因此我们只需执行Pick<T, IntesectKeys>即可获得公共属性。其余的Exclude<UnionKeys, IntersectKeys>在最终类型中是可选的。

这很丑陋,如果我感觉更好的话,我会清理它。问题是,出现在所有成分中的任何属性本身都是可选的时,仍然存在问题。有一个bug in TypeScript(从TS3.5开始),其中在{a?: string} | {a?: number}中,a属性被视为 required 属性,例如{a: string | number | undefined},而如果任何组成部分将其视为可选,则将其视为可选是更正确的。该错误会扩散到MergeUnion

type Oops = MergeUnion<{a?: string} | {a?: number}>
// type Oops =  { a: string | number | undefined; }

我的答案还不复杂,所以我在这里停止。也许这足以满足您的需求。或者,也许@ TitianCernicova-Dragomir的答案足以满足您的需求。希望这些答案对您有所帮助;祝你好运!

Link to code

答案 1 :(得分:1)

交集类型或联合类型都不会使我们进入C。联合类型(A | B仅允许访问公共属性)。交集(A & B)允许访问所有属性,但是如果属性在AB之间不一致,则该属性将是两个属性的交集(例如something将是string & Array<string>;,在这里不是很有用)。

解决方案是构建一个自定义映射类型,该映射类型将从传入的所有类型中获取键,并从每个成员中创建属性类型的并集:

interface A {
    something: string;
    somethingElse: number;
}

interface B {
    something: Array<string>;
}

type KeyOf<T> = T extends any ? keyof T : never;
type PropValue<T, K extends PropertyKey> = T extends Record<K, infer V> ? V : never;
type Merge<T> = {
    [P in KeyOf<T>] : PropValue<T, P>
}

type C = Merge<A | B>
// type C = {
//     something: string | string[];
//     somethingElse: number;
// }

KeyOf将使用一个T,如果T是一个联合,它将返回所有联合成员的密钥。它使用conditional types

的分配属性来完成此操作
type K = KeyOf<{a : number} | { b: number }> //  "a" | "b". 

这是必需的,因为keyof代表联合会只返回普通成员。 (keyof ({a : number} | { b: number })never)。

PropValue还使用条件类型的分布属性来提取键的所有值类型的并集。

type V = PropValue<{a : number} | {a : string} |{ b: number }, 'a'> //  string | number

以映射类型将其放在一起,我们得到Merge,它映射联合的每个成员中的所有键并映射到所有可能的属性类型的联合。

答案 2 :(得分:1)

谢谢@jcalz,谢谢!如果有人感兴趣,我已经对其进行了修改,以提高可读性。此外,上述错误现已解决。

type Keys<TUnion> =
  TUnion extends unknown ? keyof TUnion : never;

type Values<TObject extends Object> = {
  [TKey in keyof TObject]: TObject[TKey];
};

//

type RequiredKeys<TUnion> =
  keyof TUnion;

type RequiredValues<TUnion> =
  Pick<TUnion, RequiredKeys<TUnion>>;

//

type OptionalKeys<TUnion> =
  Exclude<Keys<TUnion>, RequiredKeys<TUnion>>;

type OptionalValue<TUnion, TKey extends PropertyKey> =
  TUnion extends Partial<Record<TKey, infer TValue>> ? TValue : never;

type OptionalValues<TUnion> = {
  [TOptionalKey in OptionalKeys<TUnion>]?: OptionalValue<TUnion, TOptionalKey>;
};

//

export type Merge<TUnion> = Values<
  RequiredValues<TUnion> &
  OptionalValues<TUnion>
>;


type Test = Merge<
  | { a?: string; b: string; c: number; }
  | { a?: number; b: string[]; c?: number; }
>;

// type Test = {
//   a?: string | number | undefined;
//   b: string | string[];
//   c?: number | undefined;
// };