I have a class which has some mandatory properties and can then have other properties defined by classes extending this one (or anyway, some extra properties I don't know about).
Now I'm trying to use Omit
type on it, defined as
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
If we define the class as
class Thing {
constructor(
public id: number,
public name: string,
public description: string
) {}
}
We can do Omit<Thing, 'id'>
and get the right type inference as Pick<Thing, "name" | "description">
If we add a [index: string]: any
to the class to support unknown properties and retry Omit<Thing, 'id'>
, now the inferred type will be Pick<Thing, string | number>
.
class Thing {
constructor(
public id: number,
public name: string,
public description: string
) {}
[index: string]: any;
}
This doesn't really make sense: when I type using the class, all properties are required even when a [index: string]: any;
is defined, why would it become a looser type when omitting one of its properties? I would expect the return type to still be Pick<Thing, "name" | "description">
.
Does anyone knows what's going on?
Omit
uses keyof
to get all the keys of a type and then excludes the omit keys from that. So a definition of Omit
might be:
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
The problem with this approach (not that there is another one) is that for types with an index signature keyof T
always just return string | number
. This is logical and is the only way it could be. What are the possible keys for Thing
for example? Well they are id
, name
and description
but also any string
so keyof might return a union of all of these id | name | description | string | number
. But string
is the base type of all string literal types. This means that string
will eat up all the other string literal types resulting in string
.
Given that keyof Thing
is string
excluding anything from string
(with Exclude<keyof T, K>
) will still result in string
, given the signature you found surprising.
Since typeScript 4.1 introduced key remapping in mapped types, you can now write a non-ugly version of Omit
which handles types with index signatures more gracefully:
type NewOmit<T, K extends PropertyKey> =
{ [P in keyof T as Exclude<P, K>]: T[P] };
You can verify that it works as desired:
type RemoveId = NewOmit<Thing, "id">;
/* type RemoveId = {
[x: string]: any;
name: string;
description: string;
} */
Nice!
Playground link to code
You can make an Omit<>
that handles index signatures, but it's really ugly.
As noted, keyof T
if T
has a string index signature is string | number
, and any literal keys (like 'id'
) get eaten up. It is possible to get the literal keys of a type with an index signature, by doing some crazy type-system hoop jumping. Once you have the literal keys and the index signature keys, you can build up an Omit
:
type _LiteralKeys<T> = {
[K in keyof T]: string extends K ? never : number extends K ? never : K
} extends { [_ in keyof T]: infer U } ? U : never;
type LiteralKeys<T> = _LiteralKeys<T> extends infer K ?
[K] extends [keyof T] ? K : never : never;
type Lookup<T, K> = K extends keyof T ? T[K] : never;
type _IndexKey<T> = string extends keyof T ?
Lookup<T, string> extends Lookup<T, number> ?
string : (string | number) : number extends keyof T ? number : never;
type IndexKey<T> = _IndexKey<T> extends infer K ?
[K] extends [keyof T] ? K : never : never;
type WidenStringIndex<T extends keyof any> =
string extends T ? (string | number) : T;
type Omit<T, K extends keyof T,
O = Pick<T, Exclude<
LiteralKeys<T>, K>> & Pick<T, Exclude<IndexKey<T>, WidenStringIndex<K>>>
> = { [P in keyof O]: O[P] }
I said it was ugly.
You can verify that it behaves as you expected:
type RemoveId = Omit<Thing, 'id'>;
// type RemoveId = {
// [x: string]: any;
// name: string;
// description: string;
// }
If you want to take the above Omit
, shove it in some library where nobody has to look at it, and use it in your code; that's up to you. I don't know that it handles all possible edge cases, or even what the "right" behavior is in some instances (e.g., you can have both a string
and a number
index where the number
values are narrower than the string
values... what do you want to see if you Omit<T, string>
on such a type? 🤷♂️) So proceed at your own risk. But I just wanted to note that a solution of sorts is possible.
Okay, hope that helps. Good luck!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With