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