Reputation: 305
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.
Upvotes: 1
Views: 437
Reputation: 327934
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>
would be seen as a supertype of 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.
Upvotes: 1