ember-cli-typescript
ember-cli-typescript copied to clipboard
[@ember/routing] private namespacing
Cross post from https://github.com/DefinitelyTyped/DefinitelyTyped/issues/37323
Which package(s) does this problem pertain to?
- [x] @types/ember__routing
What are instructions we can follow to reproduce the issue?
ember new sample; cd ./sample # Create a new ember app
ember install ember-cli-typescript # Set up typescript support
Try to add types for the beforeModel hook in a route with the transition argument
beforeModel(this: EditRoute, transition: Transition)
Observe that the transition types are imported from:
import Transition from '@ember/routing/-private/transition'
Can this be exported in a public space since this is a public API?
Thanks for filing this! Since its existence and methods are public API, I agree that the interface should be importable. We want to make sure that only an interface is importable so that the types represent the design of the actual type, which is that it is not available to import as a class and is therefore not open for arbitrary extension e.g. via sub-classing.
Any objections to exporting under those terms @jamescdavis @dfreeman @mike-north?
Let's hold off on this until we can come up with a carefully thought out answer for this question. Once we move things to a non-private space, we're obligated to support them at that location, and in this case the import statement itself is not part of Ember's public API.
Right, I'm fine with coming up with another way to support type-only exports (of which there are not very many), but we should prioritize doing so, since otherwise public types like this can't be named, which makes working with e.g. beforeModel correctly effectively impossible—you end up with any as the type because it doesn't infer that it's simply overriding the parent hook.
cc. @rwjblue for thoughts on exports of type-only items.
which makes working with e.g. beforeModel correctly effectively impossible—you end up with any as the type because it doesn't infer that it's simply overriding the parent hook.
I don't agree that it's effectively impossible. There are two workarounds
(1) import the type from a private module, as we do today
(2) use a utility type to extract the Transition type from the signature of beforeModel. This is a little ugly, but it can be done in one place and used across your whole codebase. It has pretty much no risk of breaking (we'd definitely consider it to be a bug) as long as infer keeps working the way it's designed to work today.
https://www.typescriptlang.org/play/index.html#code/JYOwLgpgTgZghgYwgAgCpTiAzsMwD2IyA3gL4BQ5CANnFlsgEr4CukJ5yXyARhDPigQAsvgAmEagAowGbLgIgAXGjk48hAJQkKFcmACeABxQAxYFCxgAglADmLALYRwAHlMA+ZAF5kp5BAAHpAgYgxScPYqoDDQyNba3l6YBsgA-PHIKiAQAG7QANyUhibIAKLBGAiQYuiY6oo+fhZWtg7ObsxsEADaAOR8AkKiEtR9ALoelDR0DMIGXexBIWFMrOzEnNyDgiLikjJqCoQqFbKINXXyGiDam9wPyFgsJlAAdDvD+9Ln14qaRQeuiAA
Let's advise people who are concerned about private imports to define something like
// app/my-types.ts
import { Route } from '@ember/routing/route';
type FirstArgument<F> = F extends (arg: infer A) => any ? A : never;
export type Transition = FirstArgument<Route['beforeModel']>;
and then they can safely
import { Transition } from './my-types'
You could even use the utility type directly for a variety of things
class MyRoute extends Route {
beforeModel(transition: FirstArgument<Route['beforeModel']>) {
super.beforeModel(transition);
}
}
This is not to say that we shouldn't give this issue some thought, but let's remove the urgency from the equation and ensure that we don't set our users up for multiple rounds of shuffling and breaking changes (as was the case when we were forced to move Registry objects).
To be clear: "prioritize" doesn't imply to me "ship it stat!" for the reason that there are workarounds. I simply mean that working out a story for these kinds of types should be a major item we consider moving forward, and that designing (not simply releasing!) a good solution for type-only exports needs to be a substantial consideration as we work to stabilize and improve type definitions in the months ahead.
I'm leery of generally recommending either of the approaches outlined here (though both work, obviously) because in the first case, imports from -private locations are also subject to breakage as a consumer; and in the second, dependence on anything which uses conditional types is (a) somewhat arcane (at least insofar as it's not especially well-taught by the docs today) and therefore not IMO appropriate for a general recommendation and (b) in danger of severe performance regressions.
To be sure, either approach serves as a minimal workaround for these kinds of type-only exports today, and I'm willing to consider publishing them as "How do I work around this problem?" guides while we stabilize things—albeit with very, very loud and clear notes about the tradeoffs associated with them. It's precisely because of the migration concerns that I'm leery to make general recommendations around them, however!
Oh, and to elaborate one of those points: I've made good use of utility types like the FirstArgument type in the past, and in fact there are several places in @dustinsoftware's codebase (with which I'm quite familiar) that use those sorts of things. It's simply that in general, my recommendation is that app code lean away from those… in part because of my experience needing both to maintain that code and to help others learn it.
One other option to consider is publishing an independent package of re-exports from this private namespace.
import { Transition } from '@typed-ember/types'
I like this solution more than anything that's been proposed so far because
(1) It allows us to continue representing Ember's public API surface (including where things may be imported from) in an accurate way
(2) It allows us to insulate consumers from thrash, in the event that we find a need to move -private things around (or move away from DT)
(3) It has absolutely no risk of colliding with anything "official ember"
As one of the main @types/ember* janitors, I'd be comfortable maintaining this, and would strongly prefer that we let the *-private* stuff remain as it is.
I'm much more comfortable with that as well, and it's a thing I've been considering for a while anyway. I'm also happy to help create and maintain it.
Hey folks, since it's been a couple years, just confirming: is @mike-north 's workaround proposed above (in-app-defined Transition type via FirstArgument helper) still the best way to move forward writing beforeModel hooks this TypeScript today?
This is sensibly the same thing but check out the documentation: https://docs.ember-cli-typescript.com/ember/routes
Basically, both recommend to reuse what is returned by public methods. That way you can't be wrong since you get exactly what it is used internally:
import type RouterService from '@ember/routing/router-service';
type Transition = ReturnType<RouterService['transitionTo']>;
Resolved as part of the definition of type-only public API for Ember in RFC 0821, in conjunction with the spec at https://www.semver-ts.org’s discussion of “user-constructibility”. The discussion here was invaluable in informing both! 🎉
Thanks!