Home > front end >  How to type-safely remove a property from a TypeScript type
How to type-safely remove a property from a TypeScript type

Time:02-01

Let's have the following two interfaces:

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

interface B {
  foo: string;
}

How can I type-safely convert an object that conforms to A to one that conforms to B?

This doesn't work:

const a = { foo: "baz", bar: 42 }
const b: B = {...a}
delete b.bar           // <=== cannot remove "bar" because B has no "bar"

I could do it without type-safety:

const a = { foo: "baz", bar: 42 }
const temp: any = {...a}
delete temp.bar
const b: B = temp

I assume this works:

const a = { foo: "baz", bar: 42 }
const b: B = {
  ...a,
  bar: undefined
}

But this is not what I want, as it assigns undefined to b.bar, which could lead to problems (e.g. Firestore won't accept it as input, as it does not allow undefined values).

Is there a type-safe way to do this?

CodePudding user response:

One thing that you can do is destructuring assigment. Something similar to this:

const a = { foo: "baz", bar: 42 }
const {foo, ...b}= a

b with contain all properties except foo. TS will be able to use infer the type for b as being compatible with B.

CodePudding user response:

What you are wanting was added in TypeScript 3.5 . It is called the Omit helper type.

Super simple, in your case it will be:

type A = {
  foo: string;
  bar: number;
}

type B = Omit<A, "bar">

To use this functionality with interfaces, you can simply throw in the extends keyword:

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

interface B extends Omit<A, "bar"> {};

// No error
const test: B = {
  foo: "hello"
};

This way you do not need extra logic to do something that can be done completely in TypeScript type system.

You can test this out in the playground here

CodePudding user response:

You could create a temporary constant and cast this to Partial<A> (playground):

const a: A = { foo: "baz", bar: 42 }
const copy = {...a}
const b: B = copy
delete (copy as Partial<A>).bar

Alternatively you could create a function like this (playground):

const reduceInterfaceTo = <Child extends Parent, Parent extends object>(
  obj: Child,
  keys: Exclude<keyof Child, keyof Parent>[]
): Parent => {
  const copy = { ...obj }
  for (const key of keys)
    delete copy[key]
  return copy
}

const a: A = { foo: "baz", bar: 42 }
const b: B = reduceInterfaceTo<A, B>(a, ['bar'])

Interestingly, this function implementation is not inherently type safe. One could change the signature to return Child while deleting properties from it. This is why we don't need to cast here. I guess this reached a limitation of Typescript.

Also note, that this functions accepts e.g. an empty list as keys parameter, so it is still up to the developer to ensure all keys that should be deleted are passed.

  • Related