AngularでTypeScriptのインポートエイリアスを使う

はいさい！ちゅらデータぬオースティンやいびーん！

# 概要

Angularでモジュールのインポートにエイリアスを付けるTypeScript機能を使う方法を紹介します。

## 背景

Angularの[正式ドキュメント](https://angular.io/docs)で「ヒーローのアプリケーションを作る」のガイドをやっている時に、「AngularでTypeScriptのエイリアスをどうやってつけるのだろう」と気になったので試験的に設定してみたら案外簡単だったので、共有したくなりました。

## インポートエイリアスとは

ESモジュールをインポートする時に、通常、相対的パスでインポートします。

しかし、サブダイレクトリが増えてくると、`import { helperA } from "../../../../../../helpers.js"`のように、とてつもなく長くなってしまいかねないのです。

また、`helpers.js`を移動させたら、大変なことに。

こういう問題を解決するために、インポートエイリアスという機能がTypeScriptにあります。

上記の`helper.js`のエイリアスを以下のように`tsconfig.json`で設定します。

```json-doc
{
    "compilerOptions": {
        "baseUrl": "./",
        "paths": {
          "@helpers": ["helper.js"]
        }
    }
}
```

すると先ほどのインポート構文が以下のように短くなります。

`import { helperA } from "@helpers";`

これが**インポートエイリアス**なのです。

# Angularでインポートエリアスを使う

Angularでインポートエイリアスを使うのは簡単です。上記の例と同じように`tsconfig.json`に`paths`の設定を追加すればできます。

Webpackと違って、Angularは`tsconfig.json`を見てくれますので特別な設定は不要です。

ガイドの[ヒーローアプリケーション](https://angular.io/generated/zips/toh-pt6/toh-pt6.zip)を作った前提で進めます！

## `types`のフォルダーに`Hero`の型を入れます。

上記のガイドでは`src/app/hero.ts`に`Hero`の型定義をエクスポートしていたのだと思いますが、今回は、`src/types`というフォルダーを作り、そこに`hero.ts`を移動させます。

```typescript:src/types/hero.ts
export interface Hero {
  id: number;
  name: string;
}
```

## tsconfigを変更する

次、上記で作ったフォルダーのエイリアスを`tsconfig.json`で指定します。

```json-doc:tsconfig.json
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
  "compileOnSave": false,
  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "./dist/out-tsc",
    // ここです
    "paths": {
      "@custom-types/*": ["src/types/*"]
    },
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "sourceMap": true,
    "declaration": false,
    "downlevelIteration": true,
    "experimentalDecorators": true,
    "moduleResolution": "node",
    "importHelpers": true,
    "target": "es2020",
    "module": "es2020",
    "lib": ["es2020", "dom"]
  },
  "angularCompilerOptions": {
    "enableI18nLegacyMessageIdFormat": false,
    "strictInjectionParameters": true,
    "strictInputAccessModifiers": true,
    "strictTemplates": true
  }
}
```

このエイリアス定義だと、`types`の中に入るすべてのファイルが使えるのでおすすめです。

なお、`@types/*`じゃなくて`@custom-types/*`と指定していることに気づかれた読者もいるかと思いますが、これには理由があります。

1. 明示的にこのプロジェクトで追加した型なのだというニュアンスを伝えたいから。
1. ___TypeScriptの`node_modules`の`@types`とコンフリクトする___ から

`@types/`で指定すると以下のようなエラーがTSインタプリターから吐かれますのでご注意を。

```
Cannot import type declaration files. Consider importing 'hero' instead of '@types/hero'
```

## エイリアスでインポートする

上記の変更を保存すれば、最後に、`Hero`がインポートされているファイルをすべて以下のようにエイリアスを使うように修正する必要があります。

```typescript:src/app/heroes/heroes.component.ts
import { Component, OnInit } from '@angular/core';
// こうして普通のエイリアス構文でインポートすれば後はAngularがやってくれます！
import { Hero } from '@custom-types/hero';
import { HeroService } from '../hero.service';
import { MessageService } from '../message.service';

@Component({
  selector: 'app-heroes',
  templateUrl: './heroes.component.html',
  styleUrls: ['./heroes.component.css'],
})
export class HeroesComponent implements OnInit {
  public heroes: Hero[] = [];
  public selectedHero?: Hero;
  protected heroesSubscription!: ReturnType<
    ReturnType<InstanceType<typeof HeroService>['getHeroes']>['subscribe']
  >;

  constructor(
    private heroService: HeroService,
    private messageService: MessageService
  ) {}

  ngOnInit(): void {
    this.getHeroes();
  }

  protected getHeroes() {
    this.heroesSubscription = this.heroService
      .getHeroes()
      .subscribe((heroes) => {
        this.heroes = heroes;
      });
  }

  public handleSelection(hero: Hero): void {
    if (hero.id === this.selectedHero?.id) {
      this.messageService.add('HeroesComponent: Hero unselected.');
      return (this.selectedHero = undefined);
    }
    this.messageService.add(`HeroesComponent: Selected ${hero.name}.`);
    this.selectedHero = hero;
  }
}
```

全てを変更し、保存してみると、エラーが消えてちゃんとコンパイルされるはずです！

# まとめ

ここまで、AngularでTypeScriptのインポートエイリアスを使う方法を紹介しましたがいかがでしょうか？

`tsconfig.json`を変更するだけであとは何もしなくていいというところがとても好きです。

Webpackでは、最初にエイリアスを設定する時にかなり苦労した記憶があります。

ViteとAstroも同じような仕組みで、`tsconfig.json`で一度設定すれば、他のややこしい設定はないのです。

## おまけ：Angularについての感想

Angularはかなり過小評価されているライブラリだと思いました。

なぜこれまでにアンチされているのか理解ができません。

Vueのように学習ハードルが低いけれど、Vueよりもツールが豊富で、全体的にオブジェクト指向のアプリケーション設計ができるようになっていてコードを整理しやすいと思います。

また、フレームワークでできる書き方に制限をかけていて、よく言えばジュニアのエンジニアでも変な書き方ができないようになっています。

悪く言えば書き方に融通が効かない、硬い感じがするのでしょうか？

クラス構文を採用しているのと、明確なライフサイクルがあるので、その中でできないことはないと筆者は思いますが、なんでヘイトされているのだろうか。

筆者は、全体的にモダンなWeb開発はDOMとブラウザ機能からかけ離れているのが良くないと思っており、Vue、Angular、LitといったライブラリのようにJavaScriptのバニラ機能を活かしているフレームワークが魅力的に感じます。