Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript - how to use utilities file project-wide with minimum ceremony?

Tags:

typescript

I've been working with Typescipt for a few months, and this aspect has been annoying me over the course of several projects already. Suppose I have the following project structure:

project
+- app
|  +- component1
|  |  +- component1.ts
|  +- component2
|  |  +- component2.ts
|  +- utils
|     +- utils.ts
+- tsconfig.json

Now, suppose that utils.ts file contains a lot of helper classes (e.g. data structures) and maybe even global functions, and almost every other file in project uses at least some part of it. How would you use it with a minimum amount of boilerplate code?

Using import statement

// utils.ts
export function foo() { return 1; }

// component1.ts
import { foo, bar, baz, ...(a long list here) } from '../utils/utils'

let x = foo(); // looks fine

Pros: imported members are simple to use

Cons: necessity to maintain long import list in almost every other file

Another option is to import everything at once:

// component1.ts
import * as utils from '../utils/utils' // I have to remember to put this line on top of every other file, but it's better than the option above

let x = utils.foo(); // unavoidable prefixes all over the code

Pros: import statement is generic and can be copypasted to every file only changing the relative path (although I'd be happier if there would be a way to do so implicitly project-wide)

Cons: namespace prefixes for simple helper functions all over the code. For some reason Typescript doesn't allow to use import * without explicit namespace.

Using triple-slash directive

// utils.ts
function foo() { return 1; } // no export keyword

// component1.ts
/// <reference path="../utils/utils.ts" />

let x = foo();

This looks much better even though I still have to specify a reference with relative path on top of every file. However, it breaks completely for me when I try to bundle my app with webpack - utils.js just doesn't get bundled along with the rest of the app (I guess this is a topic for another SO question).

What I would really like is the ability to use my code in utils.ts like I use global library classes - Map, Set, HTMLElement, Promise, etc., without any imports or prefixes, is there any way to achieve this?

P.S. Notice that I've long abandoned the good programming principle of actually splitting my utils.ts into multiple files (one per class/global function), as it would greatly exacerbate the problem above. I'm actually considering using a single file for the whole project already.

like image 678
Chriso Avatar asked Oct 29 '22 03:10

Chriso


1 Answers

How to add utils to the global scope

When you do /// <reference path="../utils/utils.ts" />, what you're telling the Typescript compiler is "everything in the utils.ts module is on the global scope". Typescript won't do anything to actually make that happen, you're just telling TS that it's done, and it believes you. (For example, if you loaded a library with a <script src=".."> tag in your HTML to load a library globally, you'd use this annotation to tell Typescript about it.)

If you want to go this route, you'll need the annotation to tell typescript that utils are in the global scope, but you'll also need to actually add logic somewhere to make that happen. Webpack has a number of ways of defining global variables, but one option would be to use global, with code like this somewhere at the start of your app:

import * as utils from "../path/to/utils";

for(const key in utils) {
    global[key] = utils[key];
}

Now all your utils are on the global scope, in reality, and the reference path annotation informs Typescript of that, so that Typescript is happy.

Why I wouldn't add utils to the global scope

But, honestly, I can't say I'd recommend doing this in practice. Polluting the global scope is a pretty classic anti-pattern, and generally makes programs tricky to follow. Anyone reading your code is just going to see "magical" functions like foo that seemingly appear out of nowhere, with no indication of where that function came from. And, like all additions to the global scope, it opens the door for name-collisions.

Personally, I recommend just doing standard imports: personally, I lean towards import { foo } from "utils/utils", though sometimes utils.foo is easier to grok (e.g. if there's a naming collision between some other foo in the module I'm importing into).

Some recommendations for making imports less painful:

Don't use relative import paths.

With a bit of config for TS and webpack, you can write your imports like

import * as utils from 'utils/utils'

Instead of:

import * as utils from '../../utils/utils'

The relevant bits of config are "include" in your tsconfig, and "resolve.modules" inside your webpack config. (Or resolve.root if you're using webpack 1.0) In your case, you'd want to add "app" to both of those lists, to indicate that "app" is the root of your source files.

Code snippets for frequently used imports

With consistent import paths, if you've got a lot of frequently used imports, you could create a code snippet for them: a bit of code that can be quickly pasted by typing a particular shortcut phrase. (They're called snippets in VSCode, Atom, and SublimeText, I assume other editors have similar functionality)

For your hypothetical math-heavy application (from the comments on the question), you might create a snippet for

import Vector from "path/to/Vector";
import Matrix from "path/to/Matrix";
import Tensor from "path/to/Tensor";

to quickly import all of those things when creating a new file.

VSCode now supports auto-imports

You can type foo and hit enter, and it'll automatically add the import for foo at the top of your file. I hate to shill for a particular editor, but if you're annoyed at the drudgery of imports, it really is pretty helpful.

Shorter namespaces

If you are using a namespace import, maybe just pick a shorter one: u.foo is only a tiny bit longer than just foo, but it's a lot less magical than monkeying with the global scope.

like image 169
Retsam Avatar answered Jan 02 '23 21:01

Retsam