-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
* refactor actionCreator typings from type
to interface
#273
Conversation
Deploy preview for redux-starter-kit-docs ready! Built with commit bc2f68e https://deploy-preview-273--redux-starter-kit-docs.netlify.com |
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit bc2f68e:
|
@phryneas Glad to see someone is putting the interface trick to good use :) |
so that they keep their name in TS errors & tooltips * fix order of operation for PayloadActionCreator
6dd6fba
to
9868ffd
Compare
260907f
to
3f5edc2
Compare
Okay, so I've done some more things:
A diff between two reports looks like this: I'll annotate it so that you can see what changed without going through all the other files :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some explanations what I did here :)
// Warning: (ae-missing-release-tag) "ActionCreatorWithNonInferrablePayload" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) | ||
// | ||
// @public (undocumented) | ||
export interface ActionCreatorWithNonInferrablePayload<T extends string = string> extends BaseActionCreator<unknown, T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added a new interface
// Warning: (ae-missing-release-tag) "ActionCreatorWithOptionalPayload" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) | ||
// | ||
// @public (undocumented) | ||
export type ActionCreatorWithOptionalPayload<P, T extends string = string> = WithTypePropertyAndMatch<{ | ||
export interface ActionCreatorWithOptionalPayload<P, T extends string = string> extends BaseActionCreator<P, T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed type to interface, external signature stays the same
|
||
// Warning: (ae-missing-release-tag) "ActionCreatorWithoutPayload" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) | ||
// | ||
// @public (undocumented) | ||
export type ActionCreatorWithoutPayload<T extends string = string> = WithTypePropertyAndMatch<() => PayloadAction<undefined, T>, T, undefined>; | ||
export interface ActionCreatorWithoutPayload<T extends string = string> extends BaseActionCreator<undefined, T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed type to interface, external signature stays the same
// Warning: (ae-missing-release-tag) "ActionCreatorWithPayload" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) | ||
// | ||
// @public (undocumented) | ||
export type ActionCreatorWithPayload<P, T extends string = string> = WithTypePropertyAndMatch<IsUnknownOrNonInferrable<P, <PT extends unknown>(payload: PT) => PayloadAction<PT, T>, <PT extends P>(payload: PT) => PayloadAction<PT, T>>, T, P>; | ||
export interface ActionCreatorWithPayload<P, T extends string = string> extends BaseActionCreator<P, T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed type to interface, external signature stays the same
// Warning: (ae-missing-release-tag) "ActionCreatorWithPreparedPayload" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) | ||
// | ||
// @public (undocumented) | ||
export type ActionCreatorWithPreparedPayload<PA extends PrepareAction<any> | void, T extends string = string> = PA extends PrepareAction<infer P> ? WithTypePropertyAndMatch<(...args: Parameters<PA>) => PayloadAction<P, T, MetaOrNever<PA>, ErrorOrNever<PA>>, T, P, MetaOrNever<PA>, ErrorOrNever<PA>> : void; | ||
export interface ActionCreatorWithPreparedPayload<Args extends unknown[], P, T extends string = string, E = never, M = never> extends BaseActionCreator<P, T, M, E> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed type ActionCreatorWithPreparedPayload
to _ActionCreatorWithPreparedPayload
. This type had a signature that made it almost unreadable for external use, but very handy for internal use.
Created new interface with the old name ActionCreatorWithPreparedPayload
, now with a more readable signature. The original renamed type essentially just redirects to this new type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@markerikson I've thought about this one, and given it's the only breaking typechange, I'd rather like to avoid that.
An alternative would be to keep the type ActionCreatorWithPreparedPayload
with the existing signature instead of renaming it and introducing a new interface to be exposed to the user.
But I'd need a new name for that and I'm not 100% sure what would be fitting. Something along the lines of PreparedActionCreator
or EnhancedActionCreator
. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this is where we get into the question of "what really is a breaking change, anyway?".
Is it a literal change to an API definition that is different than how the old one behaved? Is it something that causes runtime errors? Is it something that causes a compilation failure?
One of the downsides I've observed with TS is that almost any change can result in a compile error, even for stuff that's only intended to fix some bugs. Do those count as "breaking changes"?
Given that, I am inclined to treat stuff like this as a patch or a minor release, rather than a semver major. We've identified an issue with the types that doesn't actually affect the rest of the functionality, and we're trying to resolve that issue.
If you're worried about folks relying on that particular type, a quick search on Github turns up exactly 3 hits: 2 in RTK, and 1 where someone copy-pasted some RTK code. So I don't expect this will suddenly break people.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, that stuff gets pretty philosophical pretty fast.
In this case, I guess it's okay since the original type was very unlikely to be used by anyone (it was very unwieldy and I don't see a reason why someone would want to manually specify it anyways), as your github search confirms.
In general, I guess we'll do good to keep those type APIs as stable as possible, but that's what we've got these reports for, going forward from now. I guess people are somehow used to types breaking, but we'd probably do good to handle type-breaking changes as at least minor releases - I guess it should be possible to apply patch-level upgrades without second thought or consequence.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'm on board with that. So, this would end up as 1.2.0.
I certainly don't want to go randomly breaking types, but I do feel that types changes should be treated with a bit more leeway in semver terms.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe jaredpalmer/tsdx#365 will lead to something soon, then we could put the 1.2.0 out as a "types & tree-shaking" update.
Nice side-effect of this is that we aren't bound by tsdx 0.10 any more.
// Warning: (ae-missing-release-tag) "PayloadAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) | ||
// | ||
// @public | ||
export type PayloadAction<P = void, T extends string = string, M = never, E = never> = WithOptional<M, E, WithPayload<P, Action<T>>>; | ||
export type PayloadAction<P = void, T extends string = string, M = never, E = never> = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has to stay a type, as interfaces cannot include conditional logic (like adding and removing the property meta
depending on the type value of M
).
But I inlined the logic, so that TS resolves it into one readable type as soon as type parameters are known.
// Warning: (ae-missing-release-tag) "PayloadActionCreator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) | ||
// | ||
// @public | ||
export type PayloadActionCreator<P = void, T extends string = string, PA extends PrepareAction<P> | void = void> = IfPrepareActionMethodProvided<PA, ActionCreatorWithPreparedPayload<PA, T>, IfMaybeUndefined<P, ActionCreatorWithOptionalPayload<P, T>, IfVoid<P, ActionCreatorWithoutPayload<T>, ActionCreatorWithPayload<P, T>>>>; | ||
export type PayloadActionCreator<P = void, T extends string = string, PA extends PrepareAction<P> | void = void> = IfPrepareActionMethodProvided<PA, _ActionCreatorWithPreparedPayload<PA, T>, IsUnknownOrNonInferrable<P, ActionCreatorWithNonInferrablePayload<T>, IfVoid<P, ActionCreatorWithoutPayload<T>, IfMaybeUndefined<P, ActionCreatorWithOptionalPayload<P, T>, ActionCreatorWithPayload<P, T>>>>>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The order of operation for that type was changed to fix some bugs.
So, everything is running with all versions >= 3.3 again - from my side this is ready for you to take a look :) |
@phryneas maybe you would consider writing a blog post on writing stuff like this? I would love to learn more about TS craziness and truth to be told couldn't get any good resources, the knowledge is mostly spread across various SO answers. |
@Andarist I intend to make a talk out of this ("Taming the ternary"), and test it at a meetup early next year. Once I have that slide deck I guess I'll either make a blog post or a video out of it. But realisticly, if I really wanted to cover everything I've learned in the last half year of writing types for RTK, it'd have to be a full-day workshop ;) |
@phryneas Sorry for being off-topic, but do you have any reference links for this? I would love to learn more about it! |
Unfortunatley not - he wanted to talk about it in this talk (recommended!) but ran out of time, so I just chatted a bit with him about it. |
@phryneas Cool! This is super interesting, I always thought that it’s just about preference. Thanks a lot! |
There are some differences - in general, Types are more powerful than Interfaces as they can contain more complex maps and conditionals. |
Don't think I have any complaints about the TS aspects. But, looks like we've got a conflict with the serializable middleware. Can you clean that up? |
3f5edc2
to
bc2f68e
Compare
Okay, it's mergable again. LGTM? |
This has been in my head since @dragomirtitian made me aware that TypeScript error messages (and tooltips) are not very consistent for types, but they are for interfaces.
So the current behaviour is that the tooltip for
createAction<number, 'test'>('test')
would readAfter this PR, the same return type would read
which is WAY more descriptive and should help our users.
While doing that, I also noticed that the order of operation for the different cases of the
PayloadActionCreator
type was wrong and skipping some cases. I also added a type test for that.This is not finished yet (I have to adjust the createSlice typings and while I'm at it shorten those a bit - I've learned a new trick or two), but I thought I'd make transparent that I'm currently working on this by already opening a PR.
PS: I'm trying to stay as much to the current interfaces as possible, so this should not bring any breaking changes with it - I think by now I haven't changed anything external-facing that wasn't a bug before. The type-tests are a blessing for that 😄
PPS: ah, the joys of different behaviour in different TS versions... well, at least the CI makes me aware of it