38
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Tagged Template Literals の TypeScript推論

Last updated at Posted at 2019-10-24

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

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

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

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型として推論されます。

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

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

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

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

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

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に与えられます。

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

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

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')の様に変更すると、無事コンパイルエラーを得ることが出来ます。

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 のガードも実装に追加しました。

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

38
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
38
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?