Tagged Template Literals の TypeScript推論

React のコーディングで Tagged Template Literals は、よく使われる様になりました。styled-components にはじまり、GraphQLや lit-html などのライブラリでも使われ、お馴染みになったこの構文。さて、この Tagged Template Literals を利用した便利な関数ですが、型推論がどの様に行われているのか、気になりませんか？



# プレースホルダー関数型推論

例えば次の様な styled-components は、Function Component に適用された`Props`型を、プレースホルダー関数で引数型を勝手に推論してくれます。

```typescript
type Props = { hoge: string }
const Component: React.FC<Props> = props => (
  <div>hello world</div>
)
const StyledComponent = styled(Component)`
  ${props => props.hoge} // (property) hoge: string
`
```

これと同じ様に、自作タグ関数であっても、プレースホルダー関数で引数型が推論できる様にすることが、本日のお題です。




# Tagged Template Literals をおさらいする

これは普通の Template Literals です。当然 string型として推論されます。

```typescript
const s1 = `hoge${0}fuga` // const s1: string
// "hoge0fuga"

const s2 = `hoge${0}fuga${'1'}piyo${false}` // const s2: string
// "hoge0fuga1piyofalse"
```

タグ関数は、関数に続き Template Literals を記述することで、関数呼び出しとして実行されます。

```typescript
tagged`hoge${0}fuga${'1'}piyo${false}`
// "hoge0fuga1piyofalse"
```

このタグ関数実装は次の様になっていました。第一引数は「文字列配列」以降の引数は「プレースホルダー配列」です。関数で受け取る第一引数は`['hoge','fuga','piyo']`、以降の引数は`[0, '1', false]`となります。

```typescript
function tagged(strings, ...placeholders) {
  return strings.reduce((a, b, i) => {
    const p = placeholders[i - 1]
    return a + p + b
  })
}
tagged`hoge${0}fuga${'1'}piyo${false}`
// "hoge0fuga1piyofalse"
```


# プレースホルダーの関数を実行する

styled-components の様に、プレースホルダーに関数を与え実行するためには、どの様にすれば良いでしょうか？次の出力が目指す結果です。tagged関数で与えられた`2`が、それぞれのプレースホルダー関数引数`n`に与えられます。

```typescript
tagged(2)`hoge${n => n * 0}fuga${n => n * 1}piyo${n => n * 2}`
// "hoge0fuga2piyo4"
```

正解は次の様な実装です。HOFとして定義し、関数として評価できるプレースホルダーに対し、引数`props`を実行時に与えます。

```typescript
function tagged(props) {
  return (strings, ...placeholders) => {
    return strings.reduce((a, b, i) => {
      const p = placeholders[i - 1]
      if (typeof p === 'function') {
        return a + p(props) + b
      }
      return a + p + b
    })
  }
}
```

さて、本題はここからです。このままでは当然、型推論は効きません。この関数に対し、適切な推論が導出される様に、型情報を付与していきます。



# 関数に注釈を付与する

簡略化のため飛ばしましたが、タグ関数に渡される第1引数では特別なrawプロパティが利用でき、エスケープシーケンスが処理されない、入力された通りの生の文字列を参照できます。これを表現するための型が typescript から標準で提供されています。import をする必要はありません。

```node_modules/typescript/lib/lib.es5.d.ts
interface TemplateStringsArray extends ReadonlyArray<string> {
  readonly raw: ReadonlyArray<string>;
}
```

先に答えを言ってしまうと次の型定義で、引数n に推論が効く様になります。`tagged(2)`としているところを、`tagged('2')`の様に変更すると、無事コンパイルエラーを得ることが出来ます。

```typescript
type Placeholder<T> = (...props: T[]) => unknown
function tagged<T>(props: T) {
  return (strings: TemplateStringsArray, ...placeholders: Placeholder<T>[]) => {
    return strings.reduce((a, b, i) => {
      const p = placeholders[i - 1]
      if (typeof p === 'function') {
        return a + p(props) + b
      }
      return a + p + b
    })
  }
}
const s = tagged(2)`hoge${n => n * 0}fuga${n => n * 1}piyo${n => n * 2}`
// (parameter) n: number
```

ただしこのままでは、プレースホルダーを全て関数とする必要があります。そのため次の様に、`PrimitivePlaceholder`型と`FunctionPlaceholder`型をそれぞれ用意し、UnionTypes とすることでこの問題を解決することが出来ました。ついでに `null | undefined` のガードも実装に追加しました。

```typescript
type PrimitivePlaceholder = number | string | boolean | null | undefined
type FunctionPlaceholder<T> = (...props: T[]) => unknown
type Placeholder<T> = FunctionPlaceholder<T> | PrimitivePlaceholder
function tagged<T>(props: T) {
  return (strings: TemplateStringsArray, ...placeholders: Placeholder<T>[]) => {
    return strings.reduce((a, b, i) => {
      const p = placeholders[i - 1]
      if (typeof p === 'function') {
        return a + p(props) + b
      }
      if (p === null || p === undefined) {
        return a + b
      }
      return a + p + b
    })
  }
}
const s = tagged(2)`hoge${null}fuga${'1'}piyo${n => n * 2}`
// "hogefuga1piyo4"
```

# おわりに
実際に DefinitelyTyped から提供されている style-components の型は、これよりももっと複雑です。また、Tagged Template Literals はネストすることが可能なので、再帰的な型定義も必要になってきます。

Tagged Template Literals を利用したライブラリ推論の、基本的なベースはこのとおりなはずなので、読み解く際のヒントになれば幸いです。



参考文献：https://basarat.gitbooks.io/typescript/docs/template-strings.html