Is there a way to change the type of interface property defined in a *.d.ts
in typescript?
for example: An interface in x.d.ts
is defined as
interface A {
property: number;
}
I want to change it in the typescript files that I write to
interface A {
property: Object;
}
or even this would work
interface B extends A {
property: Object;
}
Will this approach work? It didn't work when I tried on my system. Just want to confirm if it's even possible?
I use a method that first filters the fields and then combines them.
reference Exclude property from type
interface A {
x: string
}
export type B = Omit<A, 'x'> & { x: number };
for interface:
interface A {
x: string
}
interface B extends Omit<A, 'x'> {
x: number
}
type ModifiedType = Modify<OriginalType, {
a: number;
b: number;
}>
interface ModifiedInterface extends Modify<OriginalType, {
a: number;
b: number;
}> {}
Inspired by ZSkycat's extends Omit
solution, I came up with this:
type Modify
Example:
interface OriginalInterface {
a: string;
b: boolean;
c: number;
}
type ModifiedType = Modify<OriginalInterface , {
a: number;
b: number;
}>
// ModifiedType = { a: number; b: number; c: number; }
Going step by step:
type R0 = Omit<OriginalType, 'a' | 'b'> // { c: number; }
type R1 = R0 & {a: number, b: number } // { a: number; b: number; c: number; }
type T0 = Exclude<'a' | 'b' | 'c' , 'a' | 'b'> // 'c'
type T1 = Pick<OriginalType, T0> // { c: number; }
type T2 = T1 & {a: number, b: number } // { a: number; b: number; c: number; }
v2.0 Deep Modification
interface Original {
a: {
b: string
d: {
e: string // <- will be changed
}
}
f: number
}
interface Overrides {
a: {
d: {
e: number
f: number // <- new key
}
}
b: { // <- new key
c: number
}
}
type ModifiedType = ModifyDeep<Original, Overrides>
interface ModifiedInterface extends ModifyDeep<Original, Overrides> {}
// ModifiedType =
{
a: {
b: string
d: {
e: number
f: number
}
}
b: {
c: number
}
f: number
}
Find ModifyDeep
below.
Modify
here?
type Modify<T, R> = Omit<T, keyof R> & R;
You can't change the type of an existing property.
You can add a property:
interface A {
newProperty: any;
}
But changing a type of existing one:
interface A {
property: any;
}
Results in an error:
Subsequent variable declarations must have the same type. Variable 'property' must be of type 'number', but here has type 'any'
You can of course have your own interface which extends an existing one. In that case, you can override a type only to a compatible type, for example:
interface A {
x: string | number;
}
interface B extends A {
x: number;
}
By the way, you probably should avoid using Object
as a type, instead use the type any
.
In the docs for the any
type it states:
The any type is a powerful way to work with existing JavaScript, allowing you to gradually opt-in and opt-out of type-checking during compilation. You might expect Object to play a similar role, as it does in other languages. But variables of type Object only allow you to assign any value to them - you can’t call arbitrary methods on them, even ones that actually exist:
let notSure: any = 4;
notSure.ifItExists(); // okay, ifItExists might exist at runtime
notSure.toFixed(); // okay, toFixed exists (but the compiler doesn't check)
let prettySure: Object = 4;
prettySure.toFixed(); // Error: Property 'toFixed' doesn't exist on type 'Object'.
The short answer for lazy people like me:
type Overrided = Omit<YourInterface, 'overrideField'> & { overrideField: <type> };
interface Overrided extends Omit<YourInterface, 'overrideField'> {
overrideField: <type>
}
Extending @zSkycat's answer a little, you can create a generic that accepts two object types and returns a merged type with the members of the second overriding the members of the first.
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
type Merge<M, N> = Omit<M, Extract<keyof M, keyof N>> & N;
interface A {
name: string;
color?: string;
}
// redefine name to be string | number
type B = Merge<A, {
name: string | number;
favorite?: boolean;
}>;
let one: A = {
name: 'asdf',
color: 'blue'
};
// A can become B because the types are all compatible
let two: B = one;
let three: B = {
name: 1
};
three.name = 'Bee';
three.favorite = true;
three.color = 'green';
// B cannot become A because the type of name (string | number) isn't compatible
// with A even though the value is a string
// Error: Type {...} is not assignable to type A
let four: A = three;
Omit
the property when extending the interface:
interface A {
a: number;
b: number;
}
interface B extends Omit<A, 'a'> {
a: boolean;
}
I have created this type that allows me to easily override nested interfaces:
type ModifyDeep<A extends AnyObject, B extends DeepPartialAny<A>> = {
[K in keyof A]: B[K] extends never
? A[K]
: B[K] extends AnyObject
? ModifyDeep<A[K], B[K]>
: B[K]
} & (A extends AnyObject ? Omit<B, keyof A> : A)
/** Makes each property optional and turns each leaf property into any, allowing for type overrides by narrowing any. */
type DeepPartialAny<T> = {
[P in keyof T]?: T[P] extends AnyObject ? DeepPartialAny<T[P]> : any
}
type AnyObject = Record<string, any>
And then you can use it like that:
interface Original {
a: {
b: string
d: {
e: string // <- will be changed
}
}
f: number
}
interface Overrides {
a: {
d: {
e: number
f: number // <- new key
}
}
b: { // <- new key
c: number
}
}
type ModifiedType = ModifyDeep<Original, Overrides>
interface ModifiedInterface extends ModifyDeep<Original, Overrides> {}
// ModifiedType =
{
a: {
b: string
d: {
e: number
f: number
}
}
b: {
c: number
}
f: number
}
Obj
.
DeepPartialAny
. Otherwise I converged to basically the same solution as you, so I decided to not include my version in my answer, but instead update and improve yours.
For narrowing the type of the property, simple extend
works perfect, as in Nitzan's answer:
interface A {
x: string | number;
}
interface B extends A {
x: number;
}
For widening, or generally overriding the type, you can do Zskycat's solution:
interface A {
x: string
}
export type B = Omit<A, 'x'> & { x: number };
But, if your interface A
is extending a general interface, you will lose the custom types of A
's remaining properties when using Omit
.
e.g.
interface A extends Record<string | number, number | string | boolean> {
x: string;
y: boolean;
}
export type B = Omit<A, 'x'> & { x: number };
let b: B = { x: 2, y: "hi" }; // no error on b.y!
The reason is, Omit
internally only goes over Exclude<keyof A, 'x'>
keys which will be the general string | number
in our case. So, B
would become {x: number; }
and accepts any extra property with the type of number | string | boolean
.
To fix that, I came up with a different OverrideProps
utility type as following:
type OverrideProps<M, N> = { [P in keyof M]: P extends keyof N ? N[P] : M[P] };
Example:
type OverrideProps<M, N> = { [P in keyof M]: P extends keyof N ? N[P] : M[P] };
interface A extends Record<string | number, number | string | boolean> {
x: string;
y: boolean;
}
export type B = OverrideProps<A, { x: number }>;
let b: B = { x: 2, y: "hi" }; // error: b.y should be boolean!
& N
to your OverrideProps
type, I think it will also support new props only in N. Or maybe you could union the keys P in keyof (M & N)
.
& N
it allows the overriding type to narrow props that weren't already narrowed in the base: typescriptlang.org/play?#code/…
It's funny I spend the day investigating possibility to solve the same case. I found that it not possible doing this way:
// a.ts - module
export interface A {
x: string | any;
}
// b.ts - module
import {A} from './a';
type SomeOtherType = {
coolStuff: number
}
interface B extends A {
x: SomeOtherType;
}
Cause A module may not know about all available types in your application. And it's quite boring port everything from everywhere and doing code like this.
export interface A {
x: A | B | C | D ... Million Types Later
}
You have to define type later to have autocomplete works well.
So you can cheat a bit:
// a.ts - module
export interface A {
x: string;
}
Left the some type by default, that allow autocomplete works, when overrides not required.
Then
// b.ts - module
import {A} from './a';
type SomeOtherType = {
coolStuff: number
}
// @ts-ignore
interface B extends A {
x: SomeOtherType;
}
Disable stupid exception here using @ts-ignore
flag, saying us the we doing something wrong. And funny thing everything works as expected.
In my case I'm reducing the scope vision of type x
, its allow me doing code more stricted. For example you have list of 100 properties, and you reduce it to 10, to avoid stupid situations
Date: 19/3/2021. I think the latest typescript(4.1.2) version is supporting interface
override in d.ts
file.
// in test.d.ts
interface A {
a: string
}
export interface B extends A {
a: number
}
// in any ts file
import { B } from 'test.d.ts'
// this will work
const test: B = { a: 3 }
// this will not work
const test1: B = { a: "3" }
incorrectly extends interface
ts
version? Did you declare the interface in the d.ts
file? @victorzadorozhnyy
If someone else needs a generic utility type to do this, I came up with the following solution:
/**
* Returns object T, but with T[K] overridden to type U.
* @example
* type MyObject = { a: number, b: string }
* OverrideProperty<MyObject, "a", string> // returns { a: string, b: string }
*/
export type OverrideProperty<T, K extends keyof T, U> = Omit<T, K> & { [P in keyof Pick<T, K>]: U };
I needed this because in my case, the key to override was a generic itself.
If you don't have Omit
ready, see Exclude property from type.
If you only want to modify the type of an existing property and not remove it, then & is enough:
// Style that accepts both number and percent(string)
type BoxStyle = {
height?: string | number,
width?: string | number,
padding?: string | number,
borderRadius?: string | number,
}
// These are both valid
const box1: BoxStyle = {height: '20%', width: '20%', padding: 0, borderRadius: 5}
const box2: BoxStyle = {height: 85, width: 85, padding: 0, borderRadius: 5}
// Override height and width to be only numbers
type BoxStyleNumeric = BoxStyle & {
height?: number,
width?: number,
}
// This is still valid
const box3: BoxStyleNumeric = {height: 85, width: 85, padding: 0, borderRadius: 5}
// This is not valid anymore
const box4: BoxStyleNumeric = {height: '20%', width: '20%', padding: 0, borderRadius: 5}
NOTE: Not sure if the syntax I'm using in this answer was available when the older answers were written, but I think that this is a better approach on how to solve the example mentioned in this question.
I've had some issues related to this topic (overwriting interface properties), and this is how I'm handling it:
First create a generic interface, with the possible types you'd like to use.
You can even use choose a default
value for the generic parameter as you can see in <T extends number | SOME_OBJECT = number>
type SOME_OBJECT = { foo: "bar" }
interface INTERFACE_A <T extends number | SOME_OBJECT = number> {
property: T;
}
Then you can create new types based on that contract, by passing a value to the generic parameter (or omit it and use the default):
type A_NUMBER = INTERFACE_A; // USES THE default = number TYPE. SAME AS INTERFACE_A<number>
type A_SOME_OBJECT = INTERFACE_A<SOME_OBJECT> // MAKES { property: SOME_OBJECT }
And this is the result:
const aNumber: A_NUMBER = {
property: 111 // THIS EXPECTS A NUMBER
}
const anObject: A_SOME_OBJECT = {
property: { // THIS EXPECTS SOME_OBJECT
foo: "bar"
}
}
extending Qwerty's Modify utility type solution to restrict keys of R
to ones present in T
and add IntelliSense as well
export type Modify<T, R extends Partial<Record<keyof T, any>>> = Omit<T, keyof R> & R;
Success story sharing
extend
to work by default, but alas, this littleOmit
fixes everything 🙌serverOnly
. I want server-only to never exist inModelShared
, so I use{serverOnly: never}
=> that's excellent for developer experience, they can immediately tell that the field exist, but they should useModelServer
instead. But I still have a lot of functions that expect a genericModelShared
object. However, all listed answers seems to break inheritance: if you try to pass aModelServer
, you get issues, while those types are actually related. I ended up using 3 interfaces.