16

I know that overriding properties of an interface in an extended interface, modifying their types, is forbidden.

I'm looking for an alternative solution that would allow me to not copy the contents of the first interface (it's pretty big).

Here is below my first naive approach. Given that base interface:

interface OrginalInterface {
    title?: string;
    text?: string;
    anotherProperty?: SomeType;
    // lots of other properties
}

This interface is defined in a library. I can't modify it (ie. add generics, for example) just to satisfy my needs in the extended interface.

In the extended interface, used by a wrapper library (mine), I want to reuse the existing interface, while making some fields having a different type:

interface ExtendedInterface extends OriginalInterface {
    title?: string | ReactElement<any>;
    text?: string | ReactElement<any>;
}

But this is not possible.

error TS2430: Interface 'ExtendedInterface' incorrectly extends interface 'OriginalInterface'.
  Types of property 'title' are incompatible.
    Type 'ReactElement<any>' is not assignable to type 'string'.

I also tried to merge the two interfaces together:

type Extended = OriginalInterface & NewInterfaceWithOverridingPropertiesOnly;

While this passes the compilation, it does not work. If you declare a variable with this type, you'll only be able to assign objects that have a compatible structure with OriginalInterface.

I feel like TypeScript's type-system don't offers me any other way to express my need to declare a new type derived from OrginalInterface. I don't need the new type to be assignable to OriginalInterface ; I just need it to reuse most properties of OriginalInterface.

I'd need something like mapped types with a condition on which properties are affected. Maybe Conditional types from pre-release TypeScript 2.8? Or should I copy the first interface's contents?

Morgan Touverey Quilling
  • 4,181
  • 4
  • 29
  • 41

5 Answers5

23

UPDATE, 2018-08

TypeScript 2.8 introduced Exclude<T, U> which behaves like the Diff<T, U> defined below for all types (not just key types). You should definitely use Exclude<> instead of Diff<> if you are using TypeScript 2.8 or above.

Also, in TypeScript 2.9, keyof any was expanded from string to string | number | symbol, so the below Diff<T, U> caused errors which can be fixed by changing Diff<T extends string, U extends string> to Diff<T extends keyof any, U extends keyof any>. This change has been made below.


ORIGINAL ANSWER, 2018-03

Yes, conditional types will enable this, although you can get this behavior without them as well.

The idea is to pull properties out of the original interface and then replace them with new ones. Like this:

type Diff<T extends keyof any, U extends keyof any> = 
  ({ [P in T]: P } & { [P in U]: never } & { [x: string]: never })[T];
type Overwrite<T, U> = Pick<T, Diff<keyof T, keyof U>> & U;

interface OriginalInterface {
  title?: string;
  text?: string;
  anotherProperty?: SomeType;
  // lots of other properties
}
interface Extension {
  title?: string | ReactElement<any>;
  text?: string | ReactElement<any>;
}

interface ExtendedInterface extends Overwrite<OriginalInterface, Extension> {};

const ext: ExtendedInterface = {};  // okay, no required properties
ext.text; // string | ReactElement<any> | undefined
ext.title; // string | ReactElement<any> | undefined
ext.anotherProperty; // SomeType | undefined

EDIT: I changed the definition of Overwrite<T,U> to respect the optional/required status of properties from T whose keys are not present in U.

This has the behavior you want, I think. Hope that helps; good luck!

Community
  • 1
  • 1
jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Wow, I thought TypeScript could not have my back this time, but posted here because I _knew_ there must be some trick :) But optional options seem to loose their "optionability": goo.gl/11CMyf (see the error on `test({})`, now `anotherProperty` is required). I don't know how to preserve the `?` that if that's possible, although I can workaround with Partial<> because I know that every option in both interfaces are optional, but it _could_ not be the case. – Morgan Touverey Quilling Mar 09 '18 at 18:42
  • Awesome! Didn't thought about Pick<>. Thanks to you, another JavaScript library will have type definitions :) – Morgan Touverey Quilling Mar 09 '18 at 20:31
10

Using TypeScript Omit:

Omit<T,K>

Constructs a type by picking all properties from T and then removing K

// original interface
interface A {
  a: number;
  b: number; // we want string type instead of number
}

// Remove 'b'
type BTemp = Omit<A, 'b'>;

// extends A (BTemp) and redefine b
interface B extends BTemp {
  b: string;
}

const a: B = {
  a: 5,
  b: 'B'
}

enter image description here

Felix
  • 3,999
  • 3
  • 42
  • 66
3

Breaking change since 2.9: https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#keyof-now-includes-string-number-and-symbol-keys

Use

Extract<keyof T, string> 

to avoid

Type 'keyof T' does not satisfy the constraint 'string'.

Edit: Would have written a comment for this, but lack the necessary reputation

Tucaen
  • 91
  • 1
  • 9
  • Yep. Already using it now in my recent projects, but forgot about this question. I'll had to accept this answer instead (I don't know if that follow the etiquette but I don't like when the accepted answer is outdated - very misleading). – Morgan Touverey Quilling Aug 22 '18 at 07:51
  • @MorganTouvereyQuilling I think the most reasonable behavior (etiquette, I guess) would be to edit the accepted answer instead of accepting (or creating) a comment-like answer (note how this answer does not actually answer the question itself, but points out an issue with an existing answer). Or to ask the original answerer to update their answer, as I've now done. – jcalz Aug 22 '18 at 13:45
  • You're very right - and I'm sorry that I didn't asked you to update first. Thanks again for Diff/Overwrite btw, they served across a few projects. – Morgan Touverey Quilling Aug 22 '18 at 21:18
2

Short answer:

type Overrided = Omit<YourInterface, 'overrideField'> & { overrideField: <type> }; 
Masih Jahangiri
  • 9,489
  • 3
  • 45
  • 51
1

I use this all the time for dates. I have a common library that's used both on the back-end and a front-end React application for type definitions of domain-specific entities.

When data is received via the database, the date fields are Date objects, but when consumed by the REST API, the dates come as string objects (ISO Date). Since it's unrealistic to have separate interface definitions for both (which would defeat the purpose of having a common library), I define the type in the common library as so:

type IsoDateString = string
type DatesAsIsoString<T, U extends keyof T> = Omit<T, U> & { [key in U]: IsoDateString }

// Example object
interface User {
  id: number
  registeredAt: Date
}

// on the back end
const user: User = await getUserFromDatabase()
console.log(user.registeredAt) // Date
user.registeredAt.getTime() // ms epoch

// on the front end, define
type FetchedUser = DatesAsIsoString<User, 'registeredAt'>

// then use
const user: FetchedUser = await getUserFromApi()
console.log(user.registeredAt) // string, e.g. 2020-08-25T05:52:03.000Z
const actualDate = new Date(user.registeredAt) // date object

mwieczorek
  • 2,107
  • 6
  • 31
  • 37