5

I know how to extend a TypeScript interface, but what I'm asking here is how to override a particular key in an interface. For example, say I have a simple interface A:

interface A {
    foo: string;
    bar: number;
}

And I have another simple interface B, also with a bar key, but of a different type:

interface B {
    bar: string;
    baz: number;
}

Essentially, I want to merge these interfaces into one, giving me the following interface:

interface C {
    foo: string; // from A.foo
    bar: string; // from B.bar (overrides A.bar)
    baz: number; // from B.baz
}

I wish I could just extend somehow:

interface C extends A, B {}

But I can't, because I get this error, which is totally expected and by design:

Interface 'C' cannot simultaneously extend types 'A' and 'B'. Named property 'bar' of types 'A' and 'B' are not identical.

I'm just not sure if there's another mechanism of which I am unaware. I don't think intersection or union types help here either.

4 Answers 4

9

A little late... but I fixed this issue creating another type...

type ExcludeAddress<T> = Omit<T, 'addresses'>;

This type excludes a specific key (I called 'address') from the interface...

I used it to solve a problem where I needed to cast a value of the interface...
I removed it and then created a new interface extending the old interface(without the key) + an interface with the new keyType...

Example:

type ExcludeAddress<T> = Omit<T, 'addresses'>;

export interface IAvailability {
    readonly frequency: number;
    readonly duration: number;
    readonly date: string;
    readonly addresses: Address[];
}

export interface ISerializedAvailability extends ExcludeAddress<IAvailability> {
    readonly addresses: string[];
}

You would need to adjust the ExcludeAddress I created for you own personal project...
Maybe it's not cleaneast solution, but well... it works

In my example: I created ISerializedAvailability is essencially an IAvailability where address has type string[] and not Address[]

Sign up to request clarification or add additional context in comments.

1 Comment

FYI nowadays Pick<T, Exclude<keyof T, 'addresses'>> can be written as Omit<T, 'addresses'> builtin too
3

maybe you need ?

type UnionOverrideKeys<T, U> = Omit<T, keyof U> & U;

gist link

2 Comments

You are missing link label
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.
2

To explain the reasoning a little deeper than @Alexandre Dias's example...

The & operator is known as the intersection type and basically combines or merges two types as best as it can 😉. This is similar to the union type but the union is an OR operation rather than an AND operation.

From the docs it says...

type Combined = { a: number } & { b: string };
type Conflicting = { a: number } & { a: string };

Combined has two properties, a and b, just as if they had been written as one object literal type. Intersection and union are recursive in case of conflicts, so Conflicting.a: number & string.

The important thing to note is the part about it merging the types recursively. Thus in your case, A & B would result is bar being of type number & string, which does not make sense and typescript converts this to never.

enter image description here

So any value you set as bar will throw a type error.

enter image description here

So yes the simplest solution to you case would be to just remove (i.e. Omit) the duplicate keys from one of the types before intersecting...

type CombinedAB = Omit<A, keyof B> & B

//                      👇 hack to just look at types
const test2: CombinedAB = {} as any; 
test2.bar = 'string' // ok

See demo typescript playground here


🚨🚨🚨 Helpful advice...

An INCREDIBLY useful tool for troubleshooting typescript types is piotrwitek/utility-types. This is a library of dozens of useful types to build off of the native built-in typescript types. I would highly advise adding this as a devDependencey to use whenever you need. It includes many improvements to native types as well like allowing keys for Required<T, K>.

The type that would solve your issue here is Overwrite<T, U> (From U overwrite properties to T), then you can simply go into their src code and look at how the type works...

/**
 * Overwrite
 * @desc From `U` overwrite properties to `T`
 * @example
 *   type Props = { name: string; age: number; visible: boolean };
 *   type NewProps = { age: string; other: string };
 *
 *   // Expect: { name: string; age: string; visible: boolean; }
 *   type ReplacedProps = Overwrite<Props, NewProps>;
 */
export type Overwrite<
  T extends object,
  U extends object,
  I = Diff<T, U> & Intersection<U, T>
> = Pick<I, keyof I>;

Note: Diff and Intersection are two of their custom types.

Looking at how they effectively use more complex typescript types like conditionals and infer lets you understand and create even the most complex types like DeepPartial

Comments

0

You can use a union type: type C = A | B; but then both of these work fine:

let c1: C = {
    foo: "string",
    bar: "string",
    baz: 3
};

let c2: C = {
    foo: "string",
    bar: 4,
    baz: 3
};

3 Comments

Yes, a union and an override are different. In this case, bar should not be allowed to be a number.
Right, but you can't override a property
That's what I was afraid of. I'm going to accept your answer, seeing as the answer is actually "no".

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.