5

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?

Jonas Sourlier
  • 13,684
  • 16
  • 77
  • 148
  • @MikeS. well it would work, but in a similar way as the non-typesafe way "works". It gets the job done, but it does not answer how TypeScript can actually handle this scenario. Radu's answer is better. – Jonas Sourlier Jan 31 '23 at 15:32
  • @MikeS. I agree, it would be type-safe. However I was looking for a conversion method which didn't require me to list all the common properties. – Jonas Sourlier Jan 31 '23 at 18:21

3 Answers3

7

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.

Radu Diță
  • 13,476
  • 2
  • 30
  • 34
  • This is great. Would be nice if there was a way to avoid having to list all the properties of the type `A` in the destructuring assignment. Something like `const { keyOf(A), ...b } = a`. – Jonas Sourlier Jan 31 '23 at 15:35
  • @cheesus To achieve such a functionality, you would need the keys of the interface at runtime. This either means you specify them ones e.g. in an array yourself or it boils down to the question on how to [Get keys of a Typescript interface as array of strings](https://stackoverflow.com/questions/43909566/get-keys-of-a-typescript-interface-as-array-of-strings) (which probably does not make it simpler, but could help to ensure that you don't forget some keys) – A_A Jan 31 '23 at 19:09
1

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

about14sheep
  • 1,813
  • 1
  • 11
  • 18
1

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

A_A
  • 1,832
  • 2
  • 11
  • 17