omit strict
The new Omit
type does not restrict the omitted keys to be keys actually present on the given type. There should be some avenue to express "omit, but only with keys that are present", likely either a change to Omit
or some other Omit
-like type.
Copied from https://github.com/pelotom/type-zoo/pull/31.
The benefit that a stricter type has is primarily:
Currently, permissive Omit acts as a "barrier" that prevents rename refactors from passing through, which means that any such refactor generates whole bunches of errors that have to be manually fixed. If the field in question is optional, this can actually introduce bugs.
And some further color:
I generally use Omit
with string literal unions (as opposed to, say, generic types that extends string
), because I often use them for defining higher-order React components that wrap another component _except_ for this one prop. As such, in my use case, I never want a permissive Omit
.
interface ImplementationDetailProps {
publiclyVisibleFoo: Foo;
filePrivateBar: Bar;
}
class ImplementationDetail extends React.Component<ImplementationDetailProps> { ... }
export type PublicProps = Omit<ImplementationDetailProps, "filePrivateBar">;
export class Public extends React.Component<PublicProps> { ... }
My suggestion meets these guidelines:
I was disappointed when I saw that the Omit
type added in #30552 was not the strict one suggested in #30455, and what most third-party type libraries provide. Like @seansfkelley mentioned, there are many benefits of making it strict. One additional benefit is the ability for everyone using Omit
in these type libraries to use the built-in one instead. This will not be possible if the built-in one is not strict.
// @BendingBender @CvX
Currently, permissive Omit acts as a "barrier" that prevents rename refactors from passing through
Could this be fixed instead?
Currently, permissive Omit acts as a "barrier" that prevents rename refactors from passing through
Could this be fixed instead?
That would help a lot, I think, but doesn't catch cases where someone does a manual refactor or just outright typos a field name and ends up exposing the wrong type to a consumer (i.e., a type that has fields that should not be visible).
Edit: I will say, I actually don't understand the use-case for permissive Omit
at all -- I've never wanted it, but that might be a function of the fact that I'm not generally juggling generic extends string
omits (e.g. type Foo<T extends string> = Omit<Bar, T>
) but rather literal omit (e.g. type Foo = Omit<Bar, "field">
).
It seems like the constrained Omit
type is going to make at least half of users unhappy based on declarations within DefinitelyTyped. We've decided to go with the more permissive built-in which your own constraints can build on.
I wish I hadn't encouraged opening this issue in the first place. It just made the situation worse than before as the Omit
name is now taken by TS, so we have to rename our strict versions to prevent confusion...
It seems like the constrained Omit type is going to make at least half of users unhappy based on declarations within DefinitelyTyped.
And now half of the users are unhappy because it's loose. I totally understand that decisions like this are tough, but you can't make everyone happy. Sometimes you have to make people unhappy for the greater good. And in my opinion, strict typing is the greater good.
The current situation is that everyone is still using a custom Omit
, e.g. StrictOmit
which again should be defined everywhere. Hence, we're back to start.
I like having a loose Omit option available, but I agree it would be nice to see Omit
stay consistent with how the community uses and understands it today. Providing both seems like a win-win.
Maybe we could find an alternate name for it?
OmitIf
Exclude
Remove
Without
it would be nice to see Omit stay consistent with how the community uses and understands it today.
I have to clear up this misconception. There were 12 different definitions of Omit
on DT and the two most popular definitions differed on whether or not to constraint the key:
(hit count, definition)
15
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
13
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
11
type Omit<T, K extends keyof T> = Pick<T, ({ [P in keyof T]: P } & { [P in K]: never } & { [x: string]: never, [x: number]: never })[keyof T]>;
3
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
2
type Omit<T, K extends keyof T> = Pick<T, Diff<keyof T, K>>;
2
type Omit<T, K> = { [key in Exclude<keyof T, K>]: T[key] };
2
type Omit<T1, T2> = Pick<T1, Exclude<keyof T1, keyof T2>>;
1
type Omit<T, E extends keyof T> = { ...
1
type Omit<T, K extends keyof any> = T extends any ? Pick<T, Exclude<keyof T, K>> : never;
1
type Omit<T, K extends keyof T> = Pick<T, ({ [P in keyof T]: P } & { [P in K]: never } & { [x: string]: never })[keyof T]>;
1
type Omit<T, K extends keyof T> = T extends any ? Pick<T, Exclude<keyof T, K>> : never;
1
type Omit<T, K extends string> = Pick<T, Exclude<keyof T, K>>;
You can pick at the numbers and try to declare a democratic majority or something, but the reality is that only one definition doesn't break a substantial portion of people.
Moreover there is nothing wrong with passing non-keyof T
arguments to K
:
type Omit1<T, K> = Pick<T, Exclude<keyof T, K>>;
type Omit2<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
// Can't use Omit2 here
declare function combineSpread<T1, T2>(obj: T1, otherObj: T2, rest: Omit1<T1, keyof T2>): void;
type Point3d = { x: number, y: number, z: number };
declare const p1: Point3d;
// OK
combineSpread(p1, { x: 10 }, { y: 5, z: 2 });
combineSpread(p1, { x: 1, y: 3 }, { z: 2 });
// Error
combineSpread(p1, { x: 10 }, { z: 2 });
Moreover there is nothing _wrong_ with passing non-keyof T arguments to
K
This is true, and why the original suggestion was worded to allow for "some other Omit
-like type". That said, as I noted in an earlier comment:
I actually don't understand the use-case for permissive
Omit
at all
Which is still mostly true, though your provided example seems like a thing I would eventually write at some point, I suppose. Currently, I use type-zoo
's OmitStrict
everywhere and therefore, the name collision doesn't matter to me, practically speaking.
I always default to strict rather than lenient, and if the standard library doesn't want to, that's okay. I simply figured that if the standard library was going to try to be helpful by providing what until recently was a de facto community-standard type, it would want to address all the related use-cases, and I saw an opportunity to roll more of type-zoo
into the standard library where I think it would be very helpful.
@RyanCavanaugh I appreciate that you want to look at the facts as opposed to passing opinion. Totally fair. That said, I want to make sure I'm following which packages you're talking about.
The two top packages I see, type-fest
and typical
, both look like they require the key to be present (if I'm reading them correctly).
type-fest
definition:
export type Omit<ObjectType, KeysType extends keyof ObjectType> = Pick<ObjectType, Exclude<keyof ObjectType, KeysType>>;
typical
definition:
export type Omit<T, K extends keyof T> = T extends any ? Pick<T, Exclude<keyof T, K>> : never;
You can pick at the numbers and try to declare a democratic majority or something, but the reality is that only one definition doesn't break a substantial portion of people.
@RyanCavanaugh I think this is a reasonable point.
Also worth noting that, historically, the TypeScript docs have always shown examples as the strict version.
We did not include the
Omit<T, K>
type because it is trivially written asPick<T, Exclude<keyof T, K>>
(from https://www.typescriptlang.org/docs/handbook/advanced-types.html)
All that said, I agree that Omit
should stay loose and OmitStrict
(or an alternatively named type helper) should be added in addition.
This is really misfortune to be declined. Can someone tell me what is the motivation why we add Omit
when the decision was made at 2.8 saying it's trivial??? It's definitely NOT as trivial. Also, should the doc also get updated to avoid confusion for the future developers?
In the release note, it's been said that TS core team finally realize it's so common so hoping to standardize its implementation for the community. I see it's a great move to make our life easier, but then I see it just get shipped sloppily. 馃檨
@RyanCavanaugh, I did surprise you dig into the DT and count these implementations and use it justify something that is shaky. I do appreciate this empathy to ensure not making too much noisy while ensure everyone can be happy whenever it is possible. But I do hope you and other members in the core team to rethink about the Omit
implementation. If you guys really want to make it useful while ensure the backward compatibility then perhaps we should get a much different and less frequent name like it was did on choosing naming for Exclude
over Diff
.
Also, we can consider Pick
, the closer friends of Omit
, you can see how similarity they are and how they can be complimentary to each other.
Pick
: pick members from a group to get a sub-group;
Omit
: omit members in a group to get a sub-group;
It looks pretty nature and intuitive when we see them together, isn't it? But how come Omit
is a permissive type while Pick
isn't??? _Reminder: Pick
do constrains the second typed variable._
When we make comparison, compare Omit
with Exclude
is like compare egg with chicken. Exclude is comparable with Extract
not Omit
, which should be Pick
, IMHO.
Uh... I see where this weird (Omit v.s. Exclude/Extract) comparison came from (#30738). I think it is not good to post my comment there so I did it here. Here is how I justify the API surface for how the built-in helper types could be:
non-permissive permissive
anti-selective Omit* <-----------> Exclude
^ ^
| |
v v
selective Pick <------------> Extract
For the above relationship and reason, I really believe Omit
shouldn't be permissive. If it has to be, please, please, please, find another less noisy naming for it... and so the 2.8 doc don't need to get updated too, win-win?! 馃.
UPDATE: why this is important:
In react, we sometimes want to write glue component to bridge two libraries, then we will have:
type ConstrainedComponentProps = BasicProps &
Omit<LibAProps, 'A' | 'B'> &
Pick<LibBProps, 'C' | 'D'>
That is to say, Pick
give some good IDE intellisense to catch typo, but the current Omit
didn't so it can have leaky abstraction that confused the consumed developers. Such real world use-case is really not in-common. In practice, anyone use Pick
together with Omit
would agree that making Omit
permissive is a smell decision.
I've always wanted a suggestive type, which can give both intellisense and permissive type, e.g. for Omit:
type Omit<T, K extends keyof T | keyof any> = Pick<T, Exclude<keyof T, K>>;
Or maybe just keyof (T | any)
, either way, this is against subtype reduction.
Personally I agree that Omit
should be strict, given it's the opposite of Pick
(@leoyli pretty much nailed it).
You can pick at the numbers and try to declare a democratic majority or something, but the reality is that only one definition doesn't break a substantial portion of people
which only becomes a reality when those people both update to 3.5, and remove their definition of Omit
.
Case and point, I can do this no problem:
type Pick = string;
const picked: Pick = 'hello';
I mean given that you have to update eitherway, you could have easily made both parties happy by implementing LooseOmit
& StrictOmit
, and then decided if you wanted to make a call to which one gets to be "Omit", if any.
only one definition doesn't break a substantial portion of people
That's assuming that everyone not using StrictOmit
is doing so loosely. You might have looked into this more than just the numbers, but if you've not then I don't think they're that valuable, as it depends on how they're actually being used.
For example, it's quite reasonable the people using LooseOmit
have been doing so in a "strict" sense i.e their typings don't require the looseness, it just happens to be how they wrote the typing at the time.
However, in saying all this, for me my whole actual problem with having LooseOmit
, is the lack of intellisense. It's just plain annoying, especially given Pick
, and that the whole reason you'd use a string literal with Omit
would be for a defined property (i.e LooseOmit
doesn't make any sense when you're omitting string literals)
I've found that in WebStorm intellisense if you have type Omit<T, K extends keyof T | any> = Pick<T, Exclude<keyof T, K>>;
but sadly that's not the case in playground or VSCode.
Off the top of my head (which is a head that has little idea how the internals of TS work for generating intellisense), could the intellisense somehow ignore any
in unions? i.e so that keyof T | any
would become keyof T
for the intellisense only.
That wouldn't be a breaking change, since (at least imo) what is suggested for any
is unpredictable anyway b/c of the nature of any
: you can suggest anything.
Finally, a crazy idea: what about omit.strict
and omit.loose
libs? I don't know enough about how libs work internally & in dependencies to know if that'll just straight up break, say if you uses lib: [ 'omit.strict' ]
and installed a DT package that uses loose, but that would let us pick between the two.
is it possible that a future release (3.6
, 4.0
, etc.) could tighten the strictness? i can understand the "open arms" approach here, but i really would prefer a stricter version of the type at some point for basically all the reasons mentioned so far.
If you want to enforce that no-one on your project accidentally uses the built-in Omit
, here's an ESLint rule for that:
{
rules: {
'@typescript-eslint/ban-types': [
'error',
{
types: {
Omit: "Prefer `OmitStrict`.",
},
},
],
}
}
It seems like the constrained
Omit
type is going to make at least half of users unhappy based on declarations within DefinitelyTyped.
It looks like the typescript team might have accidentally found a technical solution to what is actually a people problem: what do users _want_ this type to be? What are programmers trying to _express_ when they write Omit<Props, 'className'>
?
There are plenty of conceivable reasons an Omit
in DT might not constrain the keys. One is low-quality or under-maintained packages. Another is that the definition was written for TypeScript 2.0 which didn't have keyof
yet. I think the latest version of the stdlib should be held to a higher standard than miscellaneous DT packages. I hope we can find a path forward towards improving this situation.
Just an FYI: WebStorm 2019.2.2 will ship WEB-40482 that'll mean it will provide intellisense for the second argument of Omit
based off the first argument:
Artificially supplied Omit's second argument with the expected type containing all the keys of the first argument.
Now completion, go to declaration, find usages and rename will work for properties referenced in Omit's second argument.
Also, massive shoutout to Anton of the WebStorm team, for his amazing work improving the TypeScript side of things, and for having to put up w/ me throwing tons of TS edge-cases at him :joy:
After much discussion, we think libraries providing their own "stricter" versions of Omit (as well as Exclude
and other friends) is preferable. The Strict
versions of these types are very infectious in terms of forcing upstream constraints and it's not clear that people will wisely choose between them. This can induce some real friction if not carefully managed.
This is actually more true now that Omit
is in the lib - if we pick OmitStrict
(for which I'm sure there are in turn multiple definitions to pick from) then there's not much in terms of good names left in userspace. There are a few ways to write all of these helper types and letting developers choose the one that matches their own definition of "strict" is the option that's going to maximize individual freedom.
The reaction here and on Twitter to our (entirely defensible IMO) choice of the definition of Omit
shows why we're not really excited about picking an OmitStrict
that is going to anger some 30-70% of developers because we don't pick their preferred definition. It appears it's only really a good idea to add things to the lib if its definition is entirely unambiguous, and OmitStrict
doesn't fit that criteria. In retrospect, neither did Omit
, and perhaps we should have just left it out.
Go forth, developers, with your chosen definitions of OmitStrict
happy and secure in the knowledge that TS isn't going to stomp on that name 馃槵
Apologies for resurrecting this thread, but I did stumble upon a behavioral difference that made me very happy for my usage of OmitStrict
. I respect the team's decision to not include it in the standard library, but I thought this example was worth having recorded somewhere, and I don't believe it's been mentioned in previous comments.
I'll let the example speak for itself:
type OmitStrict<T, K extends keyof T> = T extends any ? Pick<T, Exclude<keyof T, K>> : never;
interface Foo {
foo: string;
}
// Silently produces dangerous {} type.
type T1 = Omit<Foo | undefined, "bar">;
// Complains, regardless of what you provide to the second type parameter.
type T2 = OmitStrict<Foo | undefined, "bar">;
// Complains like you would expect.
type T3 = OmitStrict<NonNullable<Foo | undefined>, "bar">;
// Ah, type safety.
type T4 = OmitStrict<NonNullable<Foo | undefined>, "foo">;
Thanks for the OmitStrict
helper, @seansfkelley.
Most helpful comment
I was disappointed when I saw that the
Omit
type added in #30552 was not the strict one suggested in #30455, and what most third-party type libraries provide. Like @seansfkelley mentioned, there are many benefits of making it strict. One additional benefit is the ability for everyone usingOmit
in these type libraries to use the built-in one instead. This will not be possible if the built-in one is not strict.// @BendingBender @CvX