Jonas Sourlier
Jonas Sourlier

Reputation: 14435

How to type-safely remove a property from a TypeScript type

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?

Upvotes: 8

Views: 7144

Answers (3)

A_A
A_A

Reputation: 1932

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 (apparently) reaches a limit of Typescript, else we would also need to cast it as Partial. (in case you're interested how to abuse the limit, try to change the return type to Child to produce unsafe results)

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. However, it ensures that the result conforms to the Parent type by only deleting properties that are special to Child

Upvotes: 1

about14sheep
about14sheep

Reputation: 1999

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

Upvotes: 1

Radu Diță
Radu Diță

Reputation: 14171

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.

Upvotes: 11

Related Questions