GitLabでForkしたプロジェクトのMR作成時、デフォルトのターゲットプロジェクトを現在のプロジェクトにする

GitLabで、フォークしたプロジェクトに対してマージリクエストを作成すると、デフォルトのターゲットブランチがフォーク元のデフォルトブランチになっている。

面倒なのでプロジェクトを切り離したりしていたが、設定からターゲットプロジェクトを現在のプロジェクトに変更できるようになっていたのでメモ。

環境

GitLab v13.11 以降。

設定

gitlab.com

GitLabの言語設定を日本語にしている場合、以下の手順で設定できる。

  1. フォークしたプロジェクトを開く
  2. 左フレームの「設定」>「一般」をクリック
  3. 左フレームの「マージリクエスト」をクリック
  4. 「Target project」を「Upstream project」から「This project」に変更
  5. 「変更を保存」ボタンをクリック

なお、「Target project」の項目はフォークしたプロジェクトでしか表示されない。

振り返り

GitLabは社内で使っているので、なかなかフォークする機会がなく気づかなかった。

今回たまたま検証のためフォークして気づいたが、これができるようになると社内でのフォーク利用もやりやすくなると思う。

ただ、フォーク先のプロジェクトで設定する必要があるのが面倒。フォーク元のプロジェクトで設定できるとより楽だったが、オープンソース開発者がサービスの主要なターゲットだと思うので、仕方ないか。

Tailwind CSSの特殊なクラスや書き方のメモ

Tailwind CSSを使い始めて3ヶ月ほど経った。

スタイルに対応したクラス以外に、特殊なクラスや書き方があったのでメモ。

環境

Tailwind CSS v3.1.6。Reactと併用する前提で記載している。

子要素にスタイルを指定

以下のStack Overflowやブログに記載があるが、クラス名として [&...] のように記述することで、子要素にスタイルを指定できる。半角スペースは _ で代用。

stackoverflow.com

tailwindcss.com

外部コンポーネントを使う場合に便利。

// Swiperのページネーションの色変更
<div
  id={paginationId}
  className="[&_.swiper-pagination-bullet-active]:bg-black"
/>

テキストの省略

テキストの省略表示 ( text... ) 用のユーティリティーが用意されている。

1行テキストの省略

truncate で設定可能。

tailwindcss.com

以下のスタイルが指定される。

overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;

複数行テキストの省略

@tailwindcss/line-clamp プラグインを導入することで、 line-clamp-行数 が使えるようになる。

https://tailwindcss.com/docs/plugins#line-clamp

3行目以降は省略する場合、 line-clamp-3 のように指定。以下のスタイルが指定される。

overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 行数;

スペーサー

space-[x|y]-サイズ で、子要素間のスペースを指定可能。

tailwindcss.com

また、フレックスレイアウトやグリッドレイアウトを使用している場合、 gap を使う。

tailwindcss.com

要素間のボーダー

並んだ要素の間にボーダーを付けることはよくあるが、 divide から始まるクラスで指定可能。

以下のように使用する。

<div class="divide-y divide-black divide-solid">
  <div>01</div>
  <div>02</div>
  <div>03</div>
</div>

important にする

! をクラス名の前に付与することで、 important にできる。Important modifierという模様。

<div className="!bg-white">...</div>

外部コンポーネントを使う場合に便利。

tailwind.config.jsimportant: true を指定することで、Tailwind CSSすべてのクラスをimportantにすることもできる。

負数

マイナスの値を使いたい場合、クラス名の先頭に - を付与する。

ネガティブマージンは、 -mt-2 のようになる。

画像の位置調整などで用いる transform: translateY(-50%) は、 -translate-y-1/2

色の透過率を指定

色指定するクラス名の後ろに /パーセンテージ を指定すると、透過してくれる。

例えば bg-black/60 は、 background-color: rgb(0 0 0 / 0.6) に解釈される。

before/after

before:クラスafter:クラス で記述可能。

contentは before:content-[値]after:content-[値] 。よくある空のcontent指定は className="before:content-['']"className='after:content-[""]'

tailwindcss.com

その他の疑似要素、疑似クラス

こちらに記載あり。接頭語としてクラス名に付与する。

tailwindcss.com

first, last は それぞれ :first-child, :last-child:first-of-type, :last-of-type はそのまま first-of-type, last-of-type

first, last, odd, even なども使用できる。

公式ドキュメントを検索しても結果が出ない場合

使い方とは異なるが、時々ハマるのでメモ。

公式ドキュメントのQuick searchは、CSSのプロパティ名で検索しても引っかからないことがある。

例えば overflow-wrap は、 Word Break としてクラス化されているが、 overflow-wrap でQuick searchしても Word Break は検索結果に含まれない。

Tailwind CSSで付けられたクラス名などを検索対象としている模様。

Tailwind側のクラス名を覚える以外の解決方法がなさそうなので、Quick searchで見つからなければGoogleinurl:https://tailwindcss.com/docs 付きで検索している。

振り返り

クラス指定する要素を起点とし、子要素に対してもスタイル指定できるため、やろうと思えばかなり自由度が高かった。

Reactでコンポーネント化する前提だが、不満なく使えている。

Quick searchのプロパティ名検索さえできるようになれば完璧か。

StorybookのDecoratorの引数を、TypeScriptで型定義する

Storybookのストーリーファイルをtsxで記述しているが、TypeScriptをstrictモードにしているため、Decoratorを定義する際、引数に型を付けないとエラーになる。

// パラメーター 'Story' の型は暗黙的に 'any' になります。
const decorator = Story => (
  <div style={{ maxWidth: '400px' }}>
    <Story />
  </div>
)

明示的に any を指定すると、今度は @typescript-eslint/no-explicit-any で警告が出るため、どんな型を指定すればいいか調べたのでメモ。

環境

Storybook v6.5.9、プロダクトコード側ではReact v18.2.0を使用。

調査と対応

StorybookのGithubを検索すると、以下のissueを発見。

github.com

export している型自体は Meta になる模様。

  • Reactの場合: import { Meta } from '@storybook/react/types-6-0'
  • Vue v3の場合: import { Meta } from '@storybook/vue3'

この Meta からソースコードを追っていくと、Reactの場合は PartialStoryFn<ReactFramework> を引数の型定義として指定できた。

import { PartialStoryFn } from '@storybook/csf'
import { ReactFramework } from '@storybook/react/types-6-0'

const decorator = (Story: PartialStoryFn<ReactFramework>) => (
  <div style={{ maxWidth: '400px' }}>
    <Story />
  </div>
)

振り返り

このために @ts-ignoreeslint-disable-next-line をつけるのもイマイチだと思っていたので、型がわかってよかった。

余談だが、はてなシンタックスハイライト、 tsx 使えないのね。

exportしているモジュールのJSDocやTSDocを必須にする

TypeScriptでの開発時、exportしているモジュールに対してはTSDocを書くようコーディング規約を定めたが、ヒアドキュメントを書く習慣のないメンバーが多く、書き漏れが頻発した。

都度コードレビューで指摘するのも面倒なので、ESLintで何とかならないかと思い、調べたのでメモ。

環境

ESLint v8.21.0、eslint-plugin-jsdoc v39.3.4。

ESLintへのプラグイン追加

昔はESLintにJSDocサポートが組み込まれていたが、廃止済み。

eslint.org

Microsofteslint-plugin-tsdocを公開しているが、これはTSDoc仕様に準拠したドキュメントかのチェックのため、やりたいこととは異なる。

上記ブログでも移行先として指定されていた、 eslint-plugin-jsdoc がTSDocにも対応している。

github.com

以下の記事で設定の実例もあった。

this.aereal.org

yarn add -D eslint-plugin-jsdoc で追加。

.eslintrcの設定

READMEを元に設定。

pluginsjsdocextendsplugin:jsdoc/recommended を追加する。

settings.jsdoc.mode で、 typescript を指定できるが、 @typescript-eslint を使用している場合は省略可能。

これでひとまず動くが、以下を変更したい。

  1. TSDocが書かれていない場合はパスするため、exportしているモジュールなどファイル外に公開されている部分には必須化
  2. 型情報があるため、 @returns および @param の型定義を省略可能とする
  3. @param がない場合や、 @param に説明がない場合、デフォルトではWarningのため、Errorにする
  4. export された typeinterface で定義したフィールドなどのTSDocも必須にする

それぞれ設定していく。

公開モジュールでのJSDoc/TSDoc必須化

jsdoc/require-jsdoc が対象。

publicOnlytrue にすると、exportされたモジュールのみ対象となる。

また、 require で対象を細かく指定できる。

  • ArrowFunctionExpression: アロー関数式
  • ClassDeclaration: クラス宣言( class MyClass { ... } )
  • ClassExpression: クラス式( const MyClass = class { ... } )
  • FunctionDeclaration: 関数宣言( function myFunction { ... } )
  • FunctionExpression: 関数式( const myFunction = function { ... } )
  • MethodDefinition: メソッド定義( const obj = { myMethod() { ... } } )

デフォルトでは FunctionDeclaration のみ true なので、すべて有効にする。

{
  "rules": {
    "jsdoc/require-jsdoc": [
      "error",
      {
        "publicOnly": true,
        "require": {
          "ArrowFunctionExpression": true,
          "ClassDeclaration": true,
          "ClassExpression": true,
          "FunctionDeclaration": true,
          "FunctionExpression": true,
          "MethodDefinition": true
        }
      }
    ]
  }
}
module.exportsのJSDocを省略可能にする

"publicOnly": true, の場合、デフォルトではES Modules形式の export ... とCommonJS形式の module.exports = ... が対象となる。

TypeScriptでコーディングする場合、 module.exports は使わないが、ライブラリ等の設定ファイルである *.config.js では module.exports が使われており、それらも警告される。

設定ファイルにJSDocを書く意味はなく、またプロダクトコード内でCommonJS形式を用いることもないため、ES Modulesのみチェックするよう、 "publicOnly": { "esm": true, "cjs": false }, に変更。

公開されたclassのプロパティや、type・interfaceでのJSDoc/TSDoc必須化

これでexportされた関数やクラスにTSDocがないとエラーが出るようになったが、クラスのプロパティや、type・interfaceおよびそのプロパティやメソッドに対してはチェックが効かない。

調べると、以下のStack Overflowの質問やissueにて、contextsに文字列を設定している。

contextsに指定できる値は、ESLintパーサーによって異なるが、AST定義名の模様。

READMEからリンクされているAST Explorerにて、コードを張り付け、パーサーを @typescript-eslint/parser にすることで、TypeScriptのAST定義名を確認できた。

AST Explorerでのパーサーの設定

構文 AST定義名
classのプロパティ PropertyDefinition
interface TSInterfaceDeclaration
type TSTypeAliasDeclaration
interfaceやtypeのプロパティ TSPropertySignature
interfaceやtypeのメソッド TSMethodSignature
enum TSEnumDeclaration
enumのメンバー TSEnumMember

これらを jsdoc/require-jsdoc の contexts に設定すると、TSDocなしではエラーが出るが、中身が空でもJSDoc/TSDocのブロック( /** ~ */ )があればエラーにならない。

説明がない場合もエラーにするには、 jsdoc/require-description の contexts にもAST定義を設定すればいいが、 contexts は追記ではなく上書きのため、 jsdoc/require-jsdoc の contexts と同じものを設定すると、 require で指定している ArrowFunctionExpression などに対するチェックが行われなくなった。

jsdoc/require-jsdoc では、「require で true にしたもの + contexts」が対象となるのに対し、その他のルールでは contexts で指定したものを対象としている模様。

以下の様に、 jsdoc/require-description の contexts に require で true にしたものも含めることで、空ブロックもエラーとなった。

    "jsdoc/require-jsdoc": [
      "error",
      {
        "publicOnly": { "esm": true, "cjs": false },
        "require": {
          "ArrowFunctionExpression": true,
          "ClassDeclaration": true,
          "ClassExpression": true,
          "FunctionDeclaration": true,
          "FunctionExpression": true,
          "MethodDefinition": true
        },
        "contexts": [
          "PropertyDefinition",
          "TSInterfaceDeclaration",
          "TSTypeAliasDeclaration",
          "TSPropertySignature",
          "TSMethodSignature",
          "TSEnumDeclaration",
          "TSEnumMember"
        ]
      }
    ],
    "jsdoc/require-description": [
      "error",
      {
        "contexts": [
          "ArrowFunctionExpression",
          "ClassDeclaration",
          "ClassExpression",
          "FunctionDeclaration",
          "FunctionExpression",
          "MethodDefinition",
          "PropertyDefinition",
          "TSInterfaceDeclaration",
          "TSTypeAliasDeclaration",
          "TSPropertySignature",
          "TSMethodSignature",
          "TSEnumDeclaration",
          "TSEnumMember"
        ]
      }
    ],

ちなみに、未検証だが、 jsdoc/require-jsdoc の require を消して、 contexts を jsdoc/require-description のものと同じように指定することも可能な模様。

追記: JavaScriptで変数を用いた場合

.eslintrcJavaScriptにし、変数を用いることで、記述をまとめることができた。

const jsRequire = {
  ArrowFunctionExpression: true,
  ClassDeclaration: true,
  ClassExpression: true,
  FunctionDeclaration: true,
  FunctionExpression: true,
  MethodDefinition: true,
}

const tsContexts = [
  'PropertyDefinition',
  'TSInterfaceDeclaration',
  'TSTypeAliasDeclaration',
  'TSPropertySignature',
  'TSMethodSignature',
  'TSEnumDeclaration',
  'TSEnumMember',
]

/** @type {import('eslint').Linter.Config} */
module.exports = {
  rules: {
    'jsdoc/require-jsdoc': [
      'error',
      {
        publicOnly: { esm: true, cjs: false },
        require: jsRequire,
        contexts: tsContexts,
      },
    ],
    'jsdoc/require-description': [
      'error',
      {
        contexts: [Object.keys(jsRequire), tsContexts].flat(),
      },
    ],
  },
}

@returns および @param の型定義を省略可能にする

@returns の省略はjsdoc/require-returns@param の型定義はjsdoc/require-param-typeが対象のため、それぞれ off に変更。

ただ、require-returnsをoffにした場合、 @returns 自体を省略することは可能になるが、戻り値の詳細を書くために @returns を明示的に記述すると、やはり型定義を書くよう警告が出る。

@returns の型定義はjsdoc/require-returns-typeのため、これも off にする。

@param がない、または説明がない場合Errorにする

jsdoc/require-param@param がない場合のルール、 jsdoc/require-param-description が説明がない場合のルールのため、それぞれ error に変更。

引数を分割代入している場合の警告の抑制

デフォルトでは引数を分割代入していると、以下の様に、それぞれの値ごとに @param を書く必要がある。

/**
 * @param root0
 * @param root0.id
 * @param root0.name
 */
function example({ id, name }: exampleArguments) {}

型情報があれば値ごとの記述は不要のため、抑制したい。

jsdoc/require-param には、渡された引数自体の @param を制御する checkDestructuredRoots と、展開された値に対する @param を制御する checkDestructured がある。

今回は展開された値への抑制ができればいいため、 checkDestructuredfalse を指定する。

これで @param の記述は不要となったが、別途 Missing @param "root0.id" といった警告が jsdoc/check-param-names にて発生するようになった。

こちらにも同様の checkDestructured が存在するため、合わせて false に設定。

{
  "rules": {
    "jsdoc/check-param-names": ["error", { "checkDestructured": false }],
    "jsdoc/require-param": ["error", { "checkDestructured": false }]
  }
}

これで、以下の様な記述でも警告が出なくなる。

/**
 * @param arguments
 */
function example({ id, name }: exampleArguments) {}

なお、 checkTypesPattern で、型名に対する正規表現による抑制もできるが、対象となるのが @param の型定義であり、TypeScriptとはそぐわなかったため checkDestructured を使用した。

export していない関数の @param の省略

exportしていないモジュールにも、TSDocで概要だけ記述することがあるが、その場合 jsdoc/require-param から @param の記載がないと警告が出る。

jsdoc/require-jsdoc の publicOnlytrue にすることでTSDocを省略できるが、省略せずに書いた場合はタグの記述が必要となる。

公開していない関数であれば、 @param の省略をできないか確認したが、対応できそうなオプションは見つけられなかった。

回避策として、 @private@internal が付与されていればルールを無効化する設定があるため、そちらを有効化する。

TSDocのドキュメント を見ると、 @private は記載がないが、 @internal は記載されている。

ただ、エディタとしてVSCodeを利用しているが、JSDoc/TSDocのブロック( /** ~ */ )内で、 @private はコード補完が効くが、 @internal は効かなかった。

VSCodeコードスニペットの追加はできるが、TSDocのブロック内でのみ有効化する、といった設定方法がわからなかったので、コーディングの負荷軽減を優先し、 @private が付与されている場合のルールを無効化するよう設定した。

{
  "settings": {
    "jsdoc": { "ignorePrivate": true }
  }
}

@internal が付与されていればルールを無効化する場合、 settings.jsdoc.ignoreInternaltrue にすればいい。

@jsxImportSource の警告の抑制

@emotion/react を使っているが、 /** @jsxImportSource @emotion/react */ に対して、 jsdoc/check-tag-names から `Invalid JSDoc tag name "jsxImportSource". が発生する。

JSX関連のタグを有効化する jsxTags が用意されているため、 true に設定する。

{
  "rules": {
    "jsdoc/check-tag-names": ["error", { "jsxTags": true }],
  }
}

断念した設定

引数名による @param の省略

現在のプロジェクトでは、Reactコンポーネントのプロパティとして コンポーネント名Properties という型を定義し、 properties という名前でコンポーネントに渡している。

コンポーネントとの関係は明らかなため、できれば @param を省略したいと思ったが、引数名での制御はできない模様。

最終的な.eslintrcへの変更

今回の設定で変更した部分は、以下の様になった。

{
  "plugins": ["jsdoc"],
  "extends": ["plugin:jsdoc/recommended"],
  "rules": {
    "jsdoc/require-jsdoc": [
      "error",
      {
        "publicOnly": { "esm": true, "cjs": false },
        "require": {
          "ArrowFunctionExpression": true,
          "ClassDeclaration": true,
          "ClassExpression": true,
          "FunctionDeclaration": true,
          "FunctionExpression": true,
          "MethodDefinition": true
        },
        "contexts": [
          "PropertyDefinition",
          "TSInterfaceDeclaration",
          "TSTypeAliasDeclaration",
          "TSPropertySignature",
          "TSMethodSignature",
          "TSEnumDeclaration",
          "TSEnumMember"
        ]
      }
    ],
    "jsdoc/check-param-names": ["error", { "checkDestructured": false }],
    "jsdoc/check-tag-names": ["error", { "jsxTags": true }],
    "jsdoc/require-description": [
      "error",
      {
        "contexts": [
          "ArrowFunctionExpression",
          "ClassDeclaration",
          "ClassExpression",
          "FunctionDeclaration",
          "FunctionExpression",
          "MethodDefinition",
          "PropertyDefinition",
          "TSInterfaceDeclaration",
          "TSTypeAliasDeclaration",
          "TSPropertySignature",
          "TSMethodSignature",
          "TSEnumDeclaration",
          "TSEnumMember"
        ]
      }
    ],
    "jsdoc/require-param": ["error", { "checkDestructured": false }],
    "jsdoc/require-param-description": "error",
    "jsdoc/require-param-type": "off",
    "jsdoc/require-returns": "off",
    "jsdoc/require-returns-type": "off"
  },
  "settings": {
    "jsdoc": { "ignorePrivate": true }
  }
}

振り返り

これでTSDocの書き漏れや、書き方の不備についてチェックできるようになった。

export していないモジュールに対してTSDocで説明だけを書きたい場合、 @private を付ける必要があるが、特に問題なく運用できている。

jsdoc/require-jsdocjsdoc/require-description のcontextsなどに指定する文字列が重複しているのがやや気持ち悪い。 .eslintrcJSONではなくJavaScriptYAMLで書いておけば、それぞれ変数やアンカーでまとめられるので、そのうち変更しようかな。

-> 2021/10/21、JavaScriptの場合を追記。

TypeScriptのinterfaceやtypeで、関数やオブジェクトの型を定義する

TypeScriptのinterfaceやtypeで、関数やオブジェクトの型を定義する際、どんな書き方をすればいいかちょこちょこ迷うのでメモ。

関数

numberを引数に取り、stringを返す関数の場合、以下のようになる。

// interfaceの場合
interface ExampleFunctionInterface {
  (value: number): string
}

// typeの場合
type ExampleFunctionType = (value: number) => string

旧ハンドブックにはinterfaceでの定義方法が記載されていたが、新しいハンドブックからはtypeでの定義方法しか見つけられなかった。

オブジェクト

インデックスシグネチャの型を指定してやればいい。

キーの型にはstring、number、Symbolが使える。

// interfaceの場合
interface StringKeyObjectInterface {
  [key: string]: unknown
}

// typeの場合
type StringKeyObjectType = {
  [key: string]: unknown
}

また、ユーティリティータイプの Record<Keys, Type> も使える。こちらのほうが可読性が高いと思う。

TypeScript: Documentation - Utility Types

type StringKeyRecord = Record<string, unknown>

ちなみに、 {}ban-types | TypeScript ESLint によると、任意のnullではないものを表すらしい。

github.com

Don't use `{}` as a type. `{}` actually means "any non-nullish value".
- If you want a type meaning "any object", you probably want `Record<string, unknown>` instead.
- If you want a type meaning "any value", you probably want `unknown` instead.
- If you want a type meaning "empty object", you probably want `Record<string, never>` instead.

Union型をキーとしたオブジェクト

ハンドブックの例にもあるが、stringやnumberからなるUnion型をRecordのキーに指定すると、キーに指定した値が過不足がある場合はコンパイルエラーにしてくれる。

type MyUnion = 'one' | 'two' | 'three'
const myUnionRecord: Record<MyUnion, number> = {
  one: 1, two: 2, three: 3,
}

インデックスシグネチャでも同様の指定ができるが、Mapped typeにする必要がある。

type MyUnion = 'one' | 'two' | 'three'
const myUnionIndexSignature: { [key in MyUnion]: number } = {
  one: 1, two: 2, three: 3,
}

複数のUnion型をキーにする場合、それぞれの型をUnion型にしてやればいい。

type MyUnion1 = 'one' | 'two' | 'three'
type MyUnion2 = 'four' | 'five' | 'six'

const myUnionRecord: Record<MyUnion1 | MyUnion2, number> = {
  one: 1, two: 2, three: 3,
  four: 4, five: 5, six: 6,
}

const myUnionIndexSignature: { [key in MyUnion1 | MyUnion2]: number } = {
  one: 1, two: 2, three: 3,
  four: 4, five: 5, six: 6,
}

キーの不足を許す場合、キーをオプショナルにすればいいが、Recordではキーが string | number | symbol である必要があり、undefinedを含むことができないため、宣言できない模様(方法があるかもしれないが、見つけられなかった)。

インデックスシグネチャの場合は、Mapped typeに ? を付けてやればいい。

type MyUnion = 'one' | 'two' | 'three'

// 以下はコンパイルエラー
// type MyUnionRecord = Record<MyUnion | undefined, number>

// こちらは問題なし
const myUnionIndexSignature: { [key in MyUnion]?: number } = {
  two: 2,
}

ただし、Union型自体にundefinedが含まれる場合、Recordと同様の理由でコンパイルエラーとなる模様。

type MyUnion = 'one' | 'two' | 'three' | undefined

// 以下はコンパイルエラー
// const myUnionIndexSignature: { [key in MyUnion]?: number } = ...

振り返り

interfaceよりtypeを使うことが多いからか、typeでの定義例は多いがinterfaceの例が少ない気がしたのでメモしておく。

また、今回参照したTypeScript ハンドブックには、他にコンストラクタの定義の方法なども記載されており、いい勉強になった。

JavaScriptのタグ付きテンプレートで、JavaのString#formatのような関数を作る

Jestでパラメーター化テストする時の it.each のような、バッククォートで囲った中で ${value} を使う書き方について調べてみたところ、タグ付きテンプレートというらしい。

また、それを使ってJavaString#format のような関数を作れたのでメモ。

タグ付きテンプレートとは

developer.mozilla.org

テンプレートリテラルの拡張形式。タグ付きテンプレートリテラル、ではない模様。

${~} がタグとなり、タグを含んだテンプレートリテラルを関数(タグ関数と呼ばれる)に渡すと、関数内で渡されたリテラルとタグを解析・加工できる。

この、タグ関数へテンプレートリテラルを渡す際の記法が、 関数名`タグ付きテンプレート` となる。

例えば、Jestの (test|it).each によるパラメーター化テストでは、以下のような記述になる。

    it.each`
      value      | expected
      ${''}      | ${'hello world'}
      ${'hello'} | ${'world'}
    `(
      '$value の場合 $expected が返る',
      ({ value, expected }) => {
        expect(doSomething(valule)).toEqual(expected)
      },
    )

やりたいこと

JavaString#format のように、リテラル内に置換用の文字列を埋め込んで、それを渡したパラメーターで置換するような関数を用意したい。

(param1, param2) => `${param1},${param2}` のように関数を用意してやればできるが、毎回関数を宣言するのは面倒。

タグ付きテンプレートでの実現方法

タグ関数から、文字列を生成する関数を、以下のように作る。

タグ関数の第1引数には、タグでSplitされた文字列の配列が渡される。 prefix${0}suffix を渡した場合、 ['prefix', 'suffix'] となる。

第2引数以降には、タグの ${~} で囲まれた数値または文字列が渡される。 ${0},${value},${1} を渡した場合、 0, value, 1 となる。

今回はTypeScriptで記述しているが、その場合、タグ関数の第1引数の型は TemplateStringsArray となる。

/**
 * 生成される関数の引数の型
 */
type MessageTemplateArgments = (string | number | boolean)[]

/**
 * 生成される関数の型
 */
interface MessageTemplate {
  (...argments: MessageTemplateArgments): string
}

/**
 * タグ関数
 */
const tagFunction =
  (templateStrings: TemplateStringsArray, ...tags: number[]): MessageTemplate =>
  (...argments) =>
    templateStrings[0] + // テンプレートの先頭に `${0}` がある場合、空文字になる
    tags
      .map((tag, index) => argments[tag] + templateStrings[index + 1])
      .join('')

タグ関数の第2引数をnumberにすることで、汎用的に使えるようにした。また、TypeScriptだと、 ${0} is ${value} のように、タグに文字列が含まれる場合はコンパイルエラーにしてくれる。

生成された関数には、実行時の引数のインデックス番号を元に、テンプレートリテラルと結合して文字列を生成している。

使い方は以下のようになる。

const template = tagFunction`私の名前は${0}${1}歳です。`
console.log(template('太郎', 30)) // 私の名前は太郎、30歳です。

引数の数や、型の指定などはできないが、毎回タグ関数を書かなくていいので楽。

(厳密にやろうと思えば、数についてはタグの数と渡された引数の数を比較したりはできそう)

振り返り

なんだこの書き方、と思って調べたのが2年ほど前。

トリビア的に「これはタグ付きテンプレートと言ってね...」みたいな話をしていたが、メッセージの国際化で、言語によって埋め込み位置を変えたいときに使えるんじゃないかと思い、試してみたらうまくハマってくれた。

タグ関数内での条件分岐などもできるので、複雑な文字列生成にも使えそうだが、シンプルな使い方でも十分メリットがあった。

追記

記事を公開した直後に、2018年にほぼ同じようなことをしている記事を見つけてしまった。

www.zu-min.com

やってることとかほぼ同じで、こっちのパクリ感がすごくて申し訳ないが、いちおうTypeScript対応しているのが違いということで。

CSSでテキストの選択を無効化する

ヘッダーやメニューで、ダブルクリックするとテキストが選択状態になるのを抑制したい。

簡単にできる方法はないかと調べると、CSSだけで選択を無効化できたのでメモ。

テキストの選択を無効にするCSS

developer.mozilla.org

user-select: none で、指定した要素とその子孫要素のテキストを選択できなくなる。

JavaScriptSelection を用いた場合は、このCSSが設定されていても要素を選択できるため、コピー防止などにはならないが、テキスト選択の抑制としては十分。

Tailwind CSSのクラス

スタイリングにTailwind CSSを使っているが、そちらだと select-none として用意されている。

User Select - Tailwind CSS

振り返り

JavaScriptで何かやらないといけないかと思ったが、CSSだけで対応できてありがたい。

対応ブラウザを見ると、IEも10から対応していたので、かなり昔からあった模様。

しばらくCSSから遠ざかっていたので、ここら辺キャッチアップしないとなぁ...