Can I Override a Key in a TypeScript Interface?
Asked Answered
D

4

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.

Devise answered 5/3, 2017 at 14:43 Comment(0)
C
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[]

Combe answered 11/7, 2019 at 23:41 Comment(1)
FYI nowadays Pick<T, Exclude<keyof T, 'addresses'>> can be written as Omit<T, 'addresses'> builtin too – Shelia
H
3

maybe you need ?

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

gist link

Hypethral answered 25/5, 2022 at 23:50 Comment(2)
You are missing link label – Variform
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. – Jerrilyn
V
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

Variform answered 26/5, 2022 at 17:22 Comment(0)
C
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
};
Compline answered 5/3, 2017 at 14:50 Comment(3)
Yes, a union and an override are different. In this case, bar should not be allowed to be a number. – Devise
Right, but you can't override a property – Compline
That's what I was afraid of. I'm going to accept your answer, seeing as the answer is actually "no". – Devise

© 2022 - 2024 β€” McMap. All rights reserved.