TypeScript で任意の文字列を受け入れつつ、文字列リテラルをサジェストしてくれる型を作る

TypeScript でライブラリを作成していると、**『基本的にはどんな文字列でも受け入れるけど、特定の文字列だと挙動が変わる』** ような引数がある関数を作成したい時があります。

この場合、引数の値をユーザーが入力しようとした時に、受け入れる特定の値を IDE がサジェストしてくれると嬉しいでしょう。

例えば、 React で `<a>` タグの `target` 属性を指定できるような関数コンポーネントを作成する場合、[`target` 属性が取りうる特殊な値 `_blank` `_parent` `_self` `_top`](https://developer.mozilla.org/ja/docs/Web/HTML/Element/a#target) がサジェストされれば、コンポーネントを使っている側は嬉しいはずです。

もしこれを素直に書いてみると、以下のようになります。

```tsx:素直に書いた型定義
type LinkProps = {
  href: string;
  target?: "_blank" | "_parent" | "_self" | "_top" | string;
};

const Link: React.FC<LinkProps> = ({ children, href, target }) => (
  <a
    href={href}
    target={target}
    rel={target === "_blank" ? "noopener noreferrer" : "noopener"}
  >
    {children}
  </a>
);
```

しかし、型を定義してみるとわかりますが、文字列リテラルが全部 `string` 型に飲まれてしまいます。

![スクリーンショット 2020-04-30 13.52.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70467/55d07488-8ad3-e3da-c2d6-8f2518b5683c.png)

文字列リテラルの情報が型から失われてしまうので、実際に使ってみても当然ながらサジェストしてくれません。

![スクリーンショット 2020-04-30 12.13.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70467/32c7de90-0bd8-da1b-845e-2b4971a126dd.png)

もちろん `string` 型を外せばサジェストされますが、 `target` 属性は実際に任意の文字列（ウィンドウ名）を取りうるので、外すわけにもいきません…。

『サジェスト』と『任意の文字列の受け入れ』を同時に機能させるには、どうすればよいでしょう？

### TL;DR

- **[`string` と区別した型を作って、文字列リテラルが飲まれないようにする](#string-と区別した型を作って文字列リテラルが飲まれないようにする)**<br />(依存関係を増やしたくない人向け)
- **[型コレクションライブラリ `type-fest` の `LiteralUnion` を使う](#型コレクションライブラリ-type-fest-の-literalunion-を使う)**<br />(面倒なことを考えたくない人向け)

前者の方法は、前提知識が無ければ「なぜこんなことしてるの？」と首を傾げたくなるような記述になるので、普通は後者の [`type-fest`][type-fest] を使えば充分でしょう。

[type-fest]: https://github.com/sindresorhus/type-fest

以下、それぞれの方法について解説していきます。

## `string` と区別した型を作って、文字列リテラルが飲まれないようにする

文字列リテラルは `string` の部分型扱いなので、 **`string` 型に飲まれるのを回避することができれば、サジェストが機能する** ようになります。

例えば、プリミティブ型の `string` の代わりに、ノンプリミティブな `String` を使うと、リテラルが飲み込まれず、サジェストを効かせながら普通の文字列も受け入れることができます。しかし、この 2 つは元々別モノで互換性がなく、[TypeScript 公式でも『`String` は使うな』と言われています](https://www.typescriptlang.org/docs/handbook/declaration-files/do-s-and-don-ts.html#general-types) ので、賢い方法とは言えません。

```tsx:ダメな例
type LinkProps = {
  href: string;
  target?: "_blank" | "_parent" | "_self" | "_top" | String; // 👈 使っちゃダメ ❌
};

const Link: React.FC<LinkProps> = ({ children, href, target }) => (
  <a
    href={href}
    target={target} // 👈 ❌ Type 'String' is not assignable to type 'string'.
    rel={target === "_blank" ? "noopener noreferrer" : undefined}
  >
    {children}
  </a>
);
```

より良いアプローチのためには、 **『`string` プリミティブ型と互換性があるが、 `string` とは区別された型を定義して使う』** 必要があります。

### 公称型

区別された型の作り方として、TypeScript で公称型を実現するテクニックに触れておきましょう。

**『公称型』** とは、Java などで採用される、型の継承関係をベースとした型付けの方法です。仮に 2 つの型が全く同じ構造を持っていても、それぞれが別個に定義されていて、同じ親が継承関係にない場合は、違う型として扱われます。

一方、TypeScript が採用している **『構造的部分型』** では、全く同じ構造の型でさえあれば、互換性がある型として扱われます。

逆に言えば、微妙に型の構造が違えば、公称型と同じように違う型として扱われるので、 **型の構造を少しずらす** ことで、`string` と区別する型を作ることができます。TypeScript において、型安全性をより高めるテクニックの 1 つです。

```typescript:公称型を作る
declare const nominalString: unique symbol;
type NominalString = string & { [nominalString]: never };

const a: NominalString = "foobar"; // 👈 公称型なので string とは区別され、代入できない ❌
```

#### 参考記事

公称型の作り方や活用については、以下の記事が詳しいのでここでは割愛。

- **[公称型を TypeScript で実現するための基礎](https://qiita.com/suin/items/ae9ed911ebab48c98835)**<br/>(https://qiita.com/suin/items/ae9ed911ebab48c98835)
- **[TypeScript: 「電話番号型」という意味を持たせた特殊なstring型の作り方](https://qiita.com/suin/items/421ae4bf99d333ccfe0d)**<br/>(https://qiita.com/suin/items/421ae4bf99d333ccfe0d)

### 互換性のある型を作る

上記の公称型を作るテクニックと、TypeScript の構造的部分型の特性をうまくミックスすると、 **『型は区別されるが、元の型と互換性のある型』** を作ることができます。

```typescript:互換性のある型を作る
declare const compatibleString: unique symbol;
type CompatibleString = string & { [compatibleString]?: never };

const a: CompatibleString = "foobar"; // 👈 代入できる ⭕️
```

ずらした型の定義が `[compatibleString]?` と、 **optional (省略可能) になっている** ことに注目してください。

`string` 型では `[compatibleString]` は常に `undefined` です。そのため、『型構造に互換性がある』ということで、 `CompatibleString` 型を `string` 型へ代入することもできます。

```typescript
// `string` は常に `[compatibleString]: undefined` なので、逆も OK
const b: string = "foobar" as CompatibleString; // 👈 ⭕️
```

つまり、`CompatibleString` 型は、`string` 型と区別なく扱えるのです。

### 文字列リテラルとの共用体型を提供する

では、作った型を文字列リテラルと組み合わせてみましょう。

```typescript
declare const targetWindowName: unique symbol;

// 新たに作る型は、目的に沿った名前にするとベター
type TargetWindowName = string & { [targetWindowName]?: never };

type LinkProps = {
  href: string;
  target?: "_blank" | "_parent" | "_self" | "_top" | TargetWindowName;
};
```

型の構造が `string` とは異なるので、文字列リテラルが飲み込まれずに済みます。

![スクリーンショット 2020-04-30 14.35.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70467/18ff3b41-ea51-1252-52c5-b63526ce753a.png)

これで、任意の文字列を受け入れつつ、IDE で特定の文字列リテラルをサジェストできるようになりました！

![スクリーンショット 2020-04-30 13.57.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70467/11b4c9e9-c74e-2c0d-bb2d-2e5b206a1bf9.png)

ちなみに、もっとシンプルに `string & {}` を使っても区別された状態になるようです (TS 3.8 で確認)。

## 型コレクションライブラリ `type-fest` の `LiteralUnion` を使う

上記に基づいて、自分で型を作っても良いですが、型コレクションライブラリ **[`type-fest`][type-fest]** が提供している **[`LiteralUnion`](https://github.com/sindresorhus/type-fest/blob/master/source/literal-union.d.ts)** を使うと、上記で説明したような処理をやってくれます。

```typescript:type-festを使う
import { LiteralUnion } from "type-fest";

type LinkProps = {
  href: string;
  target?: LiteralUnion<"_blank" | "_parent" | "_self" | "_top", string>;
};
```

「リテラルとの Union 型」という意図が明確になるので、個人的にはこっちを使うのがベターかな〜と思っています。

- [**sindresorhus/type-fest**: A collection of essential TypeScript types](https://github.com/sindresorhus/type-fest)<br/>(https://github.com/sindresorhus/type-fest)

## おわりに

TypeScript で任意の文字列を受け入れつつ、文字列リテラルをサジェストしてくれる型を作る方法についてご紹介しました。TS は良い意味で沼にハマるとディープですね…。

#### 参考文献

- [**Literal String Union Autocomplete** · Issue #29729 · microsoft/TypeScript](https://github.com/Microsoft/TypeScript/issues/29729)<br/>(https://github.com/Microsoft/TypeScript/issues/29729)