Home > Software engineering >  Clean way to convert types using delete and generics
Clean way to convert types using delete and generics

Time:10-04

I have an object of type A, that I wish to convert into type B. The easiest way with basic immutability is to shallow clone this object and delete the extraneous properties.

type A = B & {
  prop1;
}

type B = {
  prop2;
}

const example = (oldA: A): B => {
  const newB = { ...oldA };
  delete newB.prop1;
  return newB as B;
}

Without assigning the type using as, TypeScript doesn't understand the return value is the correct return type.

I don't like casting with as whenever possible as this is very error prone. If I assign the temporary variable as type A, then I cannot delete without getting an error

const newB: A = { ..oldA }

The operand of a 'delete' operator must be optional

After reading online, I see I can use this notation;

const example = (oldA: A): B => {
  const newB: Partial<Pick<A, 'prop1'>> & Omit<A, 'prop1'> = { ...oldA };
  delete newB.prop1;
  return newB;
}

This works nicely, but I want to reuse this as a generic. I have tried the generic type below but I get two errors

type PartialProperty<X, Y> = Partial<Pick<X, Y>> & Omit<X, Y>

Type 'Y' does not satisfy the constraint 'keyof X'

Type 'Y' does not satisfy the constraint 'string | number | symbol'

How can I correctly write the TypeScript definition as a reusable generic?

CodePudding user response:

The Pick<T, K> utility type requires that K be assignable to keyof T, and the Omit<T, K> utility type requires that K be assignable to some keylike type (string | number | symbol, also known as PropertyKey).

The problem with your PartialProperty definition is that your second, "key" argument (which you are calling Y but I will call K because that's more conventional) is not constrained at all. You can't pass it as the second type argument to Pick or Omit unless you constrain it to the appropriate key type. In a generic type parameter declaration like type Foo<T> = ... you can add a constraint like type Foo<T extends XXX> = ... so that any type argument passed in a T must be assignable to XXX.

Here's how it looks for PartialProperty:

type PartialProperty<X, K extends keyof X> =
    Partial<Pick<X, K>> & Omit<X, K> 

That compiles without error because now K is constrained to keyof X, which satisfies both Pick (since K extends keyof X) and Omit (since K extends keyof X which extends PropertyKey).

Playground link to code

  • Related