简体   繁体   中英

Delete object keys and return conditional type in Typescript

I have two interfaces to describe the different messages

interface MessageA {
  timestamp: number
}

interface MessageB {
  name: string
}

and an interface to describe their common fields

interface CommonMessage {
  text: string
  url: string
}

And I combine them into a new type by adding extra fields with the type declared using const enum

const enum Type {
  A,
  B,
}

type CombinedMessage =
  | (MessageA & CommonMessage & { type: Type.A })
  | (MessageB & CommonMessage & { type: Type.B })

Besides, I define a type that removes the common fields in the CombinedMessage

type DistributiveOmit<T, K extends string & keyof T> = T extends any ? Omit<T, K> : never
type Message = DistributiveOmit<CombinedMessage, keyof CommonMessage | 'type'>

Now, I want to define a function to convert CombinedMessage into Message .

const omit = <T, K extends string & keyof T>(value: T, keys: readonly K[]): DistributiveOmit<T, K> => {
  const ret = { ...value }
  for (const key of keys) {
    delete ret[key]
  }
  return ret
}

After omit function is defined, I can define

function removeCommonFields(combinedMessage: CombinedMessage): Message {
  return omit(combinedMessage, ['text', 'url', 'type'])
}

to get what I want.

However, Typescript compiler shows that the line

return ret

in function omit has errors that

Type 'T' is not assignable to type 'DistributiveOmit<T, K>'.ts(2322)

It shows that ret here is the generic type T , so I cannot return as the type DistributiveOmit<T, K> .

It seems that delete operation do nothing for the type of ret .

Currently, I use return ret as unknown as DistributiveOmit<T, K> to avoid the compiler showing error; however, I want to avoid using type assertion as possible as I can.

Is there any other ideas? Thanks.

This is more or less a design limitation of TypeScript. You can't really avoid type assertions here.

The compiler defers evaluation of conditional types that depend on an as-yet-unspecified generic type parameters, and thus it cannot really verify that anything is assignable to such types.

And while Omit<X, K> is seen as a supertype of X , and thus Omit<X, K> | Omit<Y, K> | Omit<Z, K> Omit<X, K> | Omit<Y, K> | Omit<Z, K> Omit<X, K> | Omit<Y, K> | Omit<Z, K> would be seen as a supertype of X | Y | Z X | Y | Z X | Y | Z , the compiler does not know how to generalize that distributivity to DistributiveOmit<T, K> inside the body of omit() where T is unspecified.

There is an open feature request at microsoft/TypeScript#33912 asking for more compiler support with generic functions that return conditional types. It doesn't look like there's any obvious solution to this in the near future, and even if there were it might not work in your particular example. So for the time being, the best you can do is a type assertion (or something equivalent like a single call-signature overload ).


The only slight improvement I can think of would be to use Omit<T, K> instead of unknown as your intermediate asserted type:

return ret as Omit<T, K> as DistributiveOmit<T, K>; // no error

It would help catch errors where you return something other than ret accidentally:

// return "oopsie" as unknown as DistributiveOmit<T, K>; // no error
// return "oopsie" as Omit<T, K> as DistributiveOmit<T, K>; // error!

Which might be better than nothing, but not by much.

Playground link to code

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM