Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
256 views
in Technique[技术] by (71.8m points)

node.js - TypeScript: generic interface as union of other interfaces

I would like to create a generic interface with properties that represent a union of properties from other interfaces.

Let's say I have two interfaces

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

interface B {
    something: Array<string>;
}

I do not want to write interface C as

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

because that would mean that whenever I modify either of the interfaces A or B, I would need to manually modify interface C as well.

From what I've seen in the TypeScript documentation as well as answers here on Stack Overflow, I should declare a new type

type unionOfKeys = keyof A | keyof B;

and implement generic interface form

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

I was thinking in the direction of

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

but that fails because of mismatch between a number of properties and their types.

I would appreciate any sort of help. Thank you.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

I think the following version of MergeUnion<T> might behave how you want:

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; }
// }

This is similar to the other answer in that it finds the union of all keys of all the constituents of T (call it UnionKeys, defined as T extends any ? keyof T : never) and returns a mapped type with all of them in it. The difference is that here we also find the intersection of all keys of all the constituents of T (call it IntersectKeys, defined as just keyof T) and split the keys T into two sets of keys. The one from the intersection are present in every constituent, so we can just do Pick<T, IntesectKeys> to get the common properties. The remainder, Exclude<UnionKeys, IntersectKeys> will be optional in the final type.

UPDATE 2019-08-23: the bug mentioned below seems to be fixed as of TS3.5.1

It's pretty ugly, and I'd clean it up if I felt better about it. The problem is that there's still an issue when any of the properties appearing in all constituents are themselves optional. There's a bug in TypeScript (as of TS3.5) where in {a?: string} | {a?: number}, the a property is seen as a required property like {a: string | number | undefined}, whereas it would be more correct to be treated as optional if any of the constituents have it as optional. That bug bleeds through to MergeUnion:

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

I don't have a great answer there that isn't even more complicated, so I'll stop here.

Maybe this is sufficient for your needs. Or maybe @TitianCernicova-Dragomir's answer is sufficient for your needs. Hope these answers help you; good luck!

Link to code


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...