问题
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.
回答1:
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
回答2:
Neither intersection types or union types will get us to C. A union type (A | B will only allow access to common properties). An intersection (A & B) will allow access to all properties but if the properties disagree between A and B the property will be an intersection of the two properties ( ex something will be string & Array<string>; which is not very useful here).
The solution is to build a custom mapped type that will take keys from all types passed in and create a union of property types from each member:
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 will take a T and if T is a union it will return keys of all union members. It does this using the distributive property of conditional types
type K = KeyOf<{a : number} | { b: number }> // "a" | "b".
This is needed as keyof for a unuion will only return common members. (keyof ({a : number} | { b: number }) is never).
PropValuealso uses the distributive property of conditional types to extract a union of all value types for a key.
type V = PropValue<{a : number} | {a : string} |{ b: number }, 'a'> // string | number
Putting it together in a mapped type we get Merge which maps over all keys in every member of the union and maps to a union of all possible property types.
回答3:
Thanks, @jcalz, for the great answer! I've modified it for better readability if anyone's interested. Also, the mentioned bug is now solved.
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;
// };
来源:https://stackoverflow.com/questions/56296506/typescript-generic-interface-as-union-of-other-interfaces