Open crazytoucan opened 2 years ago
There's a little confusion here because we actually have two features:
Things I like about this:
apiTypes.ts
should be apiTypes
, no action should be necessary) (please open a new issue)Things I don't like:
- Namespacyness should not be up to the exporter's file. This is just kind of weird IMO -- a module doesn't reasonably care how it gets used
Counterpoint, in a large code base there is a need for API names to be consistent and somewhat unique.
Consider this example:
import { combine } from './filePath.ts';
combine(x,y);
Suppose that someone needs to search for all calls to the combine()
function, maybe because they fixed a bug and want to audit all the various usages, to assess what should be tested. (🏆) Or maybe because we're redesigning that API and need to understand the consequences of our proposed change.
IntelliSense can help find references to combine()
, but not really in a code base with 30,000+ source files split across multiple Git repos. And not in other contexts where someone might go looking for this function:
combine()
, maybe I want to search the support forums for mention of itFor these requirements, "combine"
is a really unfortunate string to search for. Most of the search results are likely to be accidental matches of something unrelated.
Our coding conventions strongly encourage a longer name such as FilePath.combine()
:
import { FilePath } from './FilePath.ts';
FilePath.combine(x, y);
We also encourage the longer name to include a container like FilePath
. (Admittedly it can be awkward to impose meaningful groupings onto your functions, basically an arbitrary K-means clustering, but in practice this exercise tends to produce better names because it makes people think about the relationships between their code. In academia, a function is an isolated mathematical operation, but in real software engineering, functions are mostly characterized by the relationship to their callers. The algorithm isn't the point -- add one new caller and the algorithm may get rewritten!)
So... what about this:
import * as filePath from './filePath.ts';
filePath.combine(x, y);
It is mostly a solution to the original requirements. But only if we include an ESLint rule to enforce that the identifier filePath
matches the filename filePath.ts
. Otherwise someone can do this:
import * as fp from './filePath.ts';
fp.combine(x, y);
import { combine } from './filePath.ts';
Doing so would thwart our ability to search for filePath.combine()
. It would undermine the idea that filePath.combine()
is its name.
It's great for QuickFix to provide better support for people who prefer import * as filePath
. But hopefully it can ALSO provide some solution for developers facing different requirements. 😉
@octogonz all of that granted, the solution to the problem you're describing is a lint rule, not a nudge from auto-import/quickfix
@RyanCavanaugh respectfully, I disagree: You're able to write combine
and have that auto imported, but you're not able to write FilePath
and have that autoimported because there's this notion that the module name should not be up to the exported.
For one the reasoning behind this notion has never been clear imho and in practice this is not an issue. E.g. a package uploaded to NPM must have a unique name already, library authors already have to think about a unique "root identifier" for their code.
Up until here you're right - this can be done with a lint rule. But it still means that devs have to manually write star imports while not having to manually write qualified imports. This favors qualified imports which - in contrast to namespaced imports - is much more likely to lead to naming conflicts.
This issue is currently handled by defining relations between code bits in file structure which is largely unchecked and brittle and including the relations in the name of each exported code bit, leading to names that have long been turned to memes regarding e.g. Javas ProblemSolutionFactoryManagerStrategyManagerFacade
.
So the issue at hand can be supported by lint rules but to actually turn the tides a deeper tooling integration would be needed.
I really like your two "in favor" points above, can we please have that?
Also, I'm wondering... don't you have people on the team that are deeply involved in F#? What do they have to say about this? Nesting, extending and opening modules has never been easier and safer for me than in F#, I'd really like to see some similar solutions in Typescript.
Problem statement
Hey TypeScript team, I've huge respect for the software you've created for the community. I'd like to submit a request for some mechanism that improves the developer experience when trying to
import * as
for a module in your own codebase that you control. For example, if I have a file calledapiTypes.ts
, for there to be a mechanism that hints to VSCode that within this codebase, you'd prefer to import this file using star-imports (import * as apiTypes
) rather than a named import (import { User } from …
). In other words:To be clear, the specific request here is:
User
and then invoking intellisense would include an option to star-import the moduleimport * as apiTypes from "…";
, and then to insert the fully-qualified nameapiTypes.User
user: User|
->Ctrl+Space
->[ ] apiTypes.User path/to/apiTypes
->Enter
apiTypes
and then invoking intellisense would include an option to star-import the moduleimport * as apiTypes from "…";
, leaving your cursor as-is so you could then type a period.
and see module membersfunction foo(user: apiTypes|
->Ctrl+Space
->[ ] apiTypes path/to/apiTypes
->Enter
As somebody who has managed some very large codebases (1MM+ lines), the following observations are helpful for me to recognize:
* as foo from "./givenModule"
vs{ Named } from "./givenModule"
). It's likewise extremely helpful for the symbol used for its star-imports to be consistent (e.g.import * as api
vsimport * as apiTypes
vs …)User|
to either have to know you need to go add your own import by hand or using a snippet, or to eat that you're temporarily writing "nonconformant" code and to remember to run a refactoring, wait for a lint fixer, etc.resourceApi.Resource
vsenergyApi.Resource
.ResourceApiResource
orResourceApi_Resource
, but both lead to verbose-looking symbols that may be surprising for developers unfamiliar with your team's practices to understand what's going on at a glance. In contrast, star-imports are well-understood by developersnamespace
s, e.g.export namespace energyApi {}
, although it seems we're trying to move away from namespaces in practice, and they perform poorly RE: tree-shaking for runtime symbols.Resource|
->Ctrl+Space
to pull upenergyApi.Resource
. You'd first have toenergyApi
->Ctrl+Space
->Enter
->.R
->Enter
Suggestion
In an effort to simplify this problem and to be generative, how about a module-level
@pragma
directive to hint this behavior? For instance, the below top-of-file comment could be used by Intellisense to add an entry for this file, and all of its exports, to be imported asapiTypes.User
instead of the named{ User }
import.Said in different words, for files that contain a
@preferNamespaceImport
directive, Intellisense would choose to import those files using star-imports (using whatever symbol is specified in the pragma), rather than as destructured named imports.Naturally, this suggestion doesn't extend to third-party imports, like preferring lodash to be imported
* as lodash
or* as _
, etc. Again, this proposal isn't meant to be exhaustive, but in the codebases I've worked in, something like the pragma directive would go a long way to improving devX and encouraging consistent codebasesPast work and references
I recognize many iterations of this idea have been proposed in the past, so I'd like to link them here for completeness and to promote healthy discussion
🔍 Search Terms
Star imports, namespace, intellisense, autocomplete
✅ Viability Checklist
My suggestion meets these guidelines: