0

You may answer "interface A extends B { ... }" right away when you see the title, but that is not what I want.

I mean, I have two types (e.g. A and B), and I want to get a type C, which has all members of A and the members of B that have different names from members of A, and the types of members of A should can be assigned to those of B.

All of this is the same as the behavior of interface A extends B { ... }, except that it must be obtained by type formula.

In other words, I want something like this:

interface C extends B {
    [K in keyof A]: A[K];
    // this is invalid because members of interfaces must be statically determined in TS
}
// And I don't want an interface.

or like this:

// About Omit:
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-5.html#the-omit-helper-type
type C = Omit<B, keyof A> & A;
// this can be invalid when B has
//     [k: string]: any;

I want to know if I could get type C in the following form:

type A = { ... }
type B = { ... }
type C = A formula containing A and B
Fuu
  • 165
  • 2
  • 11

1 Answers1

4

Your version type C = Omit<B, keyof A> & A; seems like a good one to me, the caveat of the index signature is a general issue with mapped types.

You can get the known keys of a type using this workaround. With this we can first pick the known keys, then pick any index string index signatures and the intersect with A:

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 A = {
    a: number;
    b: string;
}

type B = {
    [a: string]: any;    
    d: number;
    b: string;
}

type C = Pick<B, Exclude<KnownKeys<B>, keyof A>> & Pick<B, string> & A;
declare let c: C;
c.d // d is still here
c[""] // index is still here

Or a more generic version:

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 A = {
    a: number;
    b: number;
}

type B = {
    [a: string]: any;    
    d: number;
    b: string;
}

type Merge<T, U> = 
    // Picking the known keys from T, requires the introduction of K as a new type parameter
    // We do this in order to make TS know K is a keyof T on the branch we do the pick
    (Exclude<KnownKeys<T>, keyof U> extends infer K? K extends keyof T ? Pick<T, K> : never: never )
    // Pick the string index signature if any 
    & (T extends Record<string, any> ? Pick<T, string> : never) 
    // We can also pick the numeric index
    & (T extends Record<number, any> ? Pick<T, number> : never) 
    // Intersect with U 
    & U;
declare let c: Merge<B, A>;
c.d // d is still here
c[""] // index is still here`
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357