The source code with samples from this post is here at GitHub (also supports fast-serve!).
Have you ever found yourself writing something like this in your SharePoint Framework web parts:
import { Customer } from '../../../../../../models/Customer';
import { Utils } from '../../../../../../common/Utils';
import { Api } from '../../../../../../services/Api';
import User from '../../../user/User';
import GreenButton from '../../../ui/green-button/GreenButton';
import Grid from '../../../../../../shared/components/grid/Grid';
Above code has a few issues:
- Readability of such code is not at the best level. A lot of parent relative paths like "../../../" don't look good
- It looks ridiculous to import "Grid" from "...components/grid/Grid". It's pretty obvious that we want to import Grid from components/grid. No need of one extra word "Grid". The same also applies to other imports
- When you add a new import or refactor your code by moving into different folders, you will have troubles figuring out how many "../../" you need :)
What if I tell you that with some webpack and typescript magic we can make it look like this:
import { Customer } from '@src/models';
import { Utils } from '@src/common';
import { Api } from '@src/services';
import User from '@hello-world-components/user';
import GreenButton from '@hello-world-components/ui/green-button';
import Grid from '@components/grid';
This code is a lot cleaner and doesn't have all mentioned issues.
Let's figure out how to do it!
index.ts
This TypeScript feature is often overlooked, yet it very powerful. Imagine you have a file Customer.ts in a folder called "models". If you want to import that file, you normally write something like that:
import { Customer } from './../models/Customer';
However, you can import Customer directly from "./models" by introducing a new file called index.ts in the "models" folder:
Re-export anything you need from "models" folder inside index.ts. In my case, it's Customer class. Of course, you are not limited to only one export and can export as many things as you need.
Inside index.ts:
export * from './Customer';
This small addition gives you a way to import Customer in a slightly shorter and elegant manner:
import { Customer } from './../models';
When TypeScript compiler encounters such imports, it will search index.ts inside models folder and will try to find export of Customer inside index.ts.
That way we got rid of "...components/grid/Grid" imports. In case of React component, a content of index.ts will be (if you use default exports):
import Grid from './Grid';
export default Grid;
Import aliases
We will use a feature called "import aliases" in order to get rid of multi-levelled "../../../". However, it requires a bit more work rather than "index.ts" feature.
Basically, we should modify webpack and typescript config to support it.
webpack
Probably you know, that we can extend webpack pipeline in SharePoint Framework with custom configurations. That's exactly what we need here. webpack has a configuration option called alias. The documentation describes it very clearly, I just want to duplicate the same here:
Create aliases to import or require certain modules more easily. For example, to alias a bunch of commonly used src/ folders:
module.exports = {
//...
resolve: {
alias: {
Utilities: path.resolve(__dirname, 'src/utilities/'),
Templates: path.resolve(__dirname, 'src/templates/')
}
}
};
Now, instead of using relative paths when importing like so:
import Utility from '../../utilities/utility';
you can use the alias:
import Utility from 'Utilities/utility';
As you're guessing this feature will help us to transform imports to "@src/module/etc".
Why do I use @ prefix? It's a common approach when working with aliases. Some libraries (like Vue.js) use it in their scaffolding tools.
Let's open gulpfule.js and modify the webpack configuration to include aliases. Insert this code right before build.initialize(gulp):
const path = require('path');
build.configureWebpack.mergeConfig({
additionalConfiguration: (generatedConfiguration) => {
if(!generatedConfiguration.resolve.alias){
generatedConfiguration.resolve.alias = {};
}
// web part specific components folder
generatedConfiguration.resolve.alias['@hello-world-components'] = path.resolve( __dirname, 'lib/webparts/helloWorld/components/')
// shared components
generatedConfiguration.resolve.alias['@components'] = path.resolve( __dirname, 'lib/shared/components/')
//root src folder
generatedConfiguration.resolve.alias['@src'] = path.resolve( __dirname, 'lib')
return generatedConfiguration;
}
});
A few notes regarding the above code:
- you should use the lib folder because it's an output folder for TypeScript. SharePoint Framework and webpack use this folder as a source of all your code
- you can have as many aliases as you want
- I have shared components folder and web part specific components, thus I added two aliases - @components and @hello-world-components (web part specific)
- I also added an alias to my root src (actually lib) path - @src
That's all cool, but if we start using these imports we will receive a lot of errors from TypeScript compiler because it knows nothing about our aliases.
TypeScript
We've done with webpack, but we should also modify TypeScript configuration tsconfig.json to support alias feature.
TypeScript compiler has an option called paths (supported only in tsconfig.json):
List of path mapping entries for module names to locations relative to the baseUrl
Sounds complicated, but take a look at what we should add and you will get the idea:
"baseUrl": ".",
"paths": {
"@hello-world-components/*": [
"src/webparts/helloWorld/components/*"
],
"@components/*": [
"src/shared/components/*"
],
"@src/*": [
"src/*"
]
},
In other words, we're telling TypeScript compiler how to map our aliased imports to real paths.
Apart from that, I found that in React SharePoint Framework template you should also update "include" section in tsconfig.json:
"include": [
"src/**/*.ts",
"src/**/*.tsx"
],
That way if you add a new .tsx file, which is not yet referenced in your code base, the compiler will still include it and vscode will produce full intellisense.
Check out how it works in action:
What's even cooler is that if you haven't imported a required class, vscode will suggest an import based on alias, if it's shorter:
That's it! This small tip makes your code a little bit better :).
If you want to take a look at the code, you will find it here at GitHub.