Svelteのチュートリアルをお試しでTypeScriptにしてみた

# はじめに

[Svelte](https://svelte.dev/) は、ReduxやVueのようなJavascriptフレームワークですが、以下のような点が異なるようです。いろいろ記事はあるようなので簡単に。

- コードの記述量が減る: その分バグが少なくなる
- 仮想DOMを使わない、ただのJavascriptとして動作する: ファイルサイズが小さくなり、構文解析や実行が速い
- フレームワーク自体にストアのサポートがある

# Svelteのチュートリアル

[公式のチュートリアル](https://svelte.dev/tutorial) にいろいろと書いてあります。
今回は、チュートリアル内`<svelte:self>`でのフォルダ階層表示をTypeScriptにしてみようと思います。
ただ、そもそもTypescriptに慣れていないためいろいろおかしいところがあると思います。

# 環境

Node.js: v14.15.3 (nvmで入れています)

# プロジェクトの準備

公式のブログ [Svelte <3 TypeScript](https://svelte.dev/blog/svelte-and-typescript) に従って準備します。
`npx`は(恒久的な)インストールをせずに実行する`npm`みたいなものっぽいです。npm v5.2.0以降で増えたらしい。そうなのか。
新しめのNode.jsをインストール済みであれば、ターミナルでプロジェクトを作成したいディレクトリを表示して、以下を実行すればSvelte+TypeScriptのプロジェクト作成は完了です。

```bash
npx degit sveltejs/template svelte-typescript-app
cd svelte-typescript-app
node scripts/setupTypeScript.js
```

npm install時になんか表示されるのが嫌なので、`package.json`を編集しておきます。

```json
{
  "name": "svelte-app",
  "version": "1.0.0",
  "private": true, // <- この行を追加
  (略)
}
```

次に、もろもろインストールします。ここまでで、Svelte+TypeScriptが動作できるようになっています。

```bash
npm install
```

せっかくなので、Sassの準備もしておきます。今回の範囲では最終的に不要でしたが...
(こちらを参考にさせていただきました: [Svelteとは？Reactの比較 / TypeScriptと Sassの導入方法](https://note.com/kawa1228/n/n3a45fab21f99) )
ちなみに、TypeScriptの準備でいいかんじになっているようなので、`rollup.config.js`の編集は不要でした。

```bash
npm install -D sass
```

最後に、VSCodeであればこちらの拡張機能を入れておくと便利そうです: [Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode)

# ここまででもろもろインストールされたバージョン

- Svelte: 3.31.1
- Typescript: 3.9.7
- Sass: 1.32.0

# コーディング

これ以降、パスは`svelte-typescript-app/src`をルート扱いとします。
[公式のチュートリアル](https://svelte.dev/tutorial) の*Introduction*のセクション(現時点で7ページ分)と、*Logic*のセクションのうち`If blocks`、`Else blocks`、`Each blocks`、を先に読んでおくとよいと思います。

### 1. ファイルのデータ型を準備

ただの型定義TypeScriptファイルです。

```typescript:repositories/Files.ts
// ファイル種類の列挙型
export enum FileType {
    // フォルダ
    folder,
    // ファイル
    file,
};

// ファイル情報1件
export type FileItem = {
    // ファイル名
    name: string;
    // ファイル種類
    type: FileType;
    // 子階層(フォルダの時用)
    children?: Array<FileItem>;
};
```

### 2. ファイルアイコン(コンポーネント)の準備

SVGファイルは、[マテリアルデザインのアイコン](https://material.io/resources/icons/?style=baseline)をいただきました。
folder, folder_open, text_snippetです。

```typescript:components/icons/FileIcon.svelte
<script lang="ts">
export let size: number = 24; // アイコンのサイズ
</script>

<style lang="scss">
svg {
    vertical-align: text-bottom;
}
</style>
    
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="{size}" viewBox="0 0 24 24" width="{size}">
  <g>
    <rect fill="none" height="24" width="24"/>
    <path d="M20.41,8.41l-4.83-4.83C15.21,3.21,14.7,3,14.17,3H5C3.9,3,3,3.9,3,5v14c0,1.1,0.9,2,2,2h14c1.1,0,2-0.9,2-2V9.83 C21,9.3,20.79,8.79,20.41,8.41z M7,7h7v2H7V7z M17,17H7v-2h10V17z M17,13H7v-2h10V13z"/>
  </g>
</svg>
```

```typescript:components/icons/FolderIcon.svelte
<script lang="ts">
export let size: number = 24; // アイコンのサイズ
export let open: boolean = false; // 開閉(true=開いている)
</script>

<style lang="scss">
svg {
    vertical-align: text-bottom;
}
</style>

<svg xmlns="http://www.w3.org/2000/svg" height={size} viewBox="0 0 24 24" width={size}>
  <path d="M0 0h24v24H0z" fill="none"/>
  {#if open}
  <path d="M20 6h-8l-2-2H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 12H4V8h16v10z"/>
  {:else}
  <path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/>
  {/if}
</svg>
```

### 3. ファイル表示のコンポーネント作成

```typescript:components/files/File.svelte
<script lang="ts">
import FileIcon from '../icons/FileIcon.svelte';

export let name: string; // ファイル名
</script>

<FileIcon size={18}/>
<span>{name}</span>
```

### 4. フォルダ表示のコンポーネント作成

開閉の処理があるのでやや長いです。

```typescript:components/files/Folder.svelte
<script lang="ts">
import FolderIcon from "../icons/FolderIcon.svelte";
import File from "./File.svelte";
import type { FileItem } from "../../repositories/Files";
import { FileType } from "../../repositories/Files";
import { slide } from 'svelte/transition';

export let folder: FileItem; // ファイル情報
export let expanded: boolean = false; // 開閉の状態(true=開いている)

// 開閉の切り替え
function toggle() {
    expanded = !expanded;
}
</script>

<style lang="scss">
div {
  cursor: pointer;
  display: inline-block;
}
ul {
  list-style-type: none;
  padding-inline-start: 20px;
}
li {
  margin-block-start: 5px;
}
li:first-child {
  margin-block-start: 0;
}
p {
  margin-block: 0;
  padding-inline-start: 20px;
}
</style>

<div on:click={toggle}>
  <FolderIcon size={18} open={expanded} />
  <span class:expanded>{folder.name}</span>
</div>

{#if expanded}
{#if folder.children && folder.children.length > 0}
<ul transition:slide>
  {#each folder.children as item}
    <li>
      {#if item.type == FileType.folder}
      <svelte:self folder={item} />
      {:else}
      <File {...item}/>
      {/if}
    </li>
  {/each}
</ul>
{:else}
<p transition:slide>空っぽだよ</p>
{/if}
{/if}
```

### 5. App.svelte作成

```typescript:components/App.svelte
<script lang="ts">
import Folder from './files/Folder.svelte';
import type { FileItem } from '../repositories/Files';
import { FileType } from '../repositories/Files';

// 表示するフォルダ階層の内容
const root: FileItem = {
    name: 'root',
    type: FileType.folder,
    children: [
        {
            name: 'file1',
            type: FileType.file,
        },
        {
            name: 'folder2',
            type: FileType.folder,
        },
        {
            name: 'folder3',
            type: FileType.folder,
            children: [
                {
                    name: 'file2',
                    type: FileType.file,
                },
                {
                    name: 'folder4',
                    type: FileType.folder,
                    children: [],
                },
            ],
        },
    ],
};
</script>

<h1>Explorer</h1>
<Folder folder={root} />
```

### 6. main.ts修正

```typescript:main.ts
//import App from './App.svelte'; // 元のApp.svelte
import App from './components/App.svelte'; // 新しいApp.svelte

const app = new App({
	target: document.body,
	props: {
		name: 'world'
	}
});

export default app;
```

# 最終的なみため

ターミナルから起動します。Svelte+TypeScript+SassがJavascriptとCSSにコンパイルされ、ブラウザで表示可能となります。

```bash
cd svelte-typescript-app
npm run dev
```

localhostの5000番ポートで起動するので、ブラウザで http://localhost:5000 にアクセスすると、以下のように表示されます(フォルダを開閉できます)。
また、ソースコードの変更を保存すると自動で表示が変わります。変わらなくなった場合はブラウザでリロードしてみてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642815/4570383b-2a20-4c4e-db27-c4c471cc7e15.png)

`npm run build`すれば、難読化されたJSファイルが出力されます。
出力先は public/build です。`npm run dev`と同じ場所なので、互いに上書きされます。(rollup.config.jsを編集すればbuldとdevで出力先を変えられるとは思います)

# はまったところ

TypeScript 3.8以降で、`import type`が増えたみたいです。
ただの`import`だとコンパイルのエラーになりました。

今回は以上です。続くかも。

# 参考まとめ

[Svelte](https://svelte.dev/): Svelte公式トップ
[Svelte <3 TypeScript](https://svelte.dev/blog/svelte-and-typescript): Svelte公式のTypescript導入方法
[Svelteとは？Reactの比較 / TypeScriptと Sassの導入方法](https://note.com/kawa1228/n/n3a45fab21f99): Sass導入参考
[Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode): VSCodeのSvelte拡張機能
[Icons - Material Design](https://material.io/resources/icons/?style=baseline): マテリアルデザインのアイコン一覧