2021-04-14
|~4 min read
|624 words
Update May 22, 2021: I’ve now tried this a number of times since writing this originally and nearly every time I run into problems where it doesn’t work as expected. Despite reading the documentation and feeling pretty good about following the patterns / updating both the
baseUrl
and thepaths
as documented in the TS handbook. It appears that this is not the intended use of this feature and Typescript doesn’t officially support it as of March 2020 at least (source).I’ve spent enough time working through different examples. I may return to it at a later date, but I’m moving on from the problem for now.If you want to look at the problem, I have a CodeSandbox that I feel should work… but isn’t. If you see the problem, please ping me!
Update November 29, 2021: The trailing
/*
on the key and value of the paths is significant. Without it, Typescript will only resolve to that directory, which is unlikely the desired behavior. I have updated the CodeSandbox to show two separate solutions.
Quality of life enhancement for typescript projects is to use absolute imports.
Let’s walk through how we can go from:
import { Button } from "../../../Button"
import { utilFn } from "../../../utils/specialUtils"
to:
import { Button } from "components/Button"
import { utilFn } from "utils/specialUtils"
Imagining for the time being that the directory structure of the project looks a bit like:
.
├── src
│ ├── components
│ └── utils
└── test
There are actually two updates that we’ll want to make:
paths
in the compilerOptions
to point to these directories so that they can be treated as modules.include
option to ensure that they’re actually included in the compiled output.Note: If the project is already using typescript, we can make the change in the tsconfig.json
. If not, we can use an jsconfig.json
.
The changes might look like the following
{
"compilerOptions": {
"baseUrl": "src", //relative to where the tsconfig is
"paths": {
"*": ["./*"],
"test/*": ["../test/*"],
"components/*": ["src/components/*"],
"utils/*": ["src/utils/*"]
}
}
}
With this configuration, we’ll map all of the modules as top level relative to src
. Note that test
has to also be mapped relative to the baseUrl
, which was made src
. I’ve seen many configurations where the root of the project directory (where the tsconfig
lives) is used as the baseUrl
, in practice, I’ve always had a hard time with getting that to work.
This enables a much better developer experience. Not only are modules resolved as top level entities, but the developer can “click through” to see the source code as well. It also allows modules to be namespaced, for example:
import { Button } from "Button"
import { utilFn } from "specialUtils"
Though, because we also called out the components/*
and utils/*
, we can still do what we set out to do:
import { Button } from "components/Button"
import { utilFn } from "utils/specialUtils"
Because it can sometimes be challenging to understand what’s a “local” module vs a true imported module, two common practices I’ve observed are:
@
or a #
For example
{
"compilerOptions": {
"baseUrl": "src", //relative to where the tsconfig is
"paths": {
"internal/*": ["./*"],
"@test/*": ["../test/*"],
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"]
}
}
}
And that should be all that’s necessary so that when Typescript compiles the code, it will resolve the import statements accordingly and improve the developer experience along the way.
Additional reading / resources:
Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!