🌟

心理学実験作成で仲良くなるTypeScript

2021/12/25に公開

はじめに

2021 年 10 月に jsPsychv7系から はnpmパッケージとして公開され,Node.jsを開発環境として利用しやすくなりました。前回の記事ではNode.js環境で jsPsych を用いた心理実験を作成する方法を紹介しました。

個人的に以前からNode.js環境に興味があったのですが,その理由の一つとして,TypeScript を利用できることが挙げられます。TypeScript は「型(type)を定義のできる JavaScript」です。型を定義していると実行前にバグに気づくことができたりします。個人的には,型定義によって変数や関数の役割(用途?仕様?)が明確になってコーディング時の迷いが減るように思います。その他詳細は以下のサイト・記事などを参照してください。

jsPsych がNode.jsに対応したことで jsPsych ベースの心理学実験も TypeScript でかけるようになりました。そこで今回は TypeScript を利用した心理学実験作成について紹介します。

本記事は psyJS Advent Calendar 2021 25 日目の記事になります。

TypeScript の導入

Node.jswebpackの導入はすでに済んでいて,以下のようなディレクトリ構成であることを前提に話を進めます。この構成は,前回の記事で用いたものです。前回の記事にはNode.jswebpackの導入についても書いてあります。それらの導入がまだの場合は参考にしてみてください。

.
├── dist
├── node_modules
├── package-lock.json
├── package.json
├── src
└── webpack.config.js

新たに TypeScript を利用するためにnpmtypescriptts-loaderをインストールしてください。

npm install typescript ts-loader

typescriptは今回のテーマである TypeScript です。ts-loaderwebpackで TypeScript のコードをバンドルするために必要なモジュールです。

TypeScript 環境の設定を行います。以下のコマンドを実行すると設定ファイル tsconfig.jsonが生成されます。

npx tsc --init

ファイル内を確認するとほとんどのオプションがコメントアウトされています。次の2点について変更します。

  • modulees6に変更(webpack 公式に準拠)
  • moduleResolutionというオプションのコメントアウトを外す

有効な行だけを取り出すと,以下のようになります。ハイライトのかかっている部分はデフォルトから変更した部分です。

tsconfig.json
  {
    "compilerOptions": {
      "target": "es5",
-     "module": "CommonJS",
+     "module": "es6",
+     "moduleResolution": "node",
      "esModuleInterop": true ,
      "forceConsistentCasingInFileNames": true,
      "strict": true,
      "skipLibCheck": true
    }
  }

実験を作成する

TypeScript を使って,実験を作成してみます。型定義をガシガシ使ってみたかったので,前回の記事のhello-worldよりも実践的なものとして,拙著チュートリアルのフランカー課題(の一部)を例として使用しています。

サンプルコード(100 行弱あるのでたたみました)
flanker.ts
import { initJsPsych } from 'jspsych';
import htmlKeyboardResponse from '@jspsych/plugin-html-keyboard-response';
import 'jspsych/css/jspsych.css';

// Type definitions ----------------------

type FlankerVariables = {
  stim: string;
  condition: '一致' | '不一致';
  answer: 'f' | 'j';
};

type DataHTMLKeyboardResponse = {
  response: string;
  rt: number;
  stimulus: string;
  [s: string]: string | number;
};

type AnyData = { [s: string]: string | number | (<T>() => T) };

type TrialHTMLKeyboardResponse = {
  type: typeof htmlKeyboardResponse;
  stimulus: string | (() => string);
  trial_duration?: number;
  choices?: string[] | 'NO_KEYS' | 'ALL_KEYS';
  data?: AnyData;
  on_finish?: (data: DataHTMLKeyboardResponse) => void;
};

type Block = {
  timeline: TrialHTMLKeyboardResponse[];
  timeline_variables: FlankerVariables[];
  sample?: {
    type: 'fixed-repetitions';
    size: number;
  };
};

// Task ---------------------------

const jsPsych = initJsPsych({
  on_finish: function (): void {
    jsPsych.data.displayData();
  },
});

const font_size = 48;

const stimsFlankerMain: FlankerVariables[] = [
  { stim: '<<<<<', condition: '一致', answer: 'f' },
  { stim: '>>>>>', condition: '一致', answer: 'j' },
  { stim: '>><>>', condition: '不一致', answer: 'f' },
  { stim: '<<><<', condition: '不一致', answer: 'j' },
];

const getVarFlanker = (varname: keyof FlankerVariables): string => jsPsych.timelineVariable(varname);

const fixation: TrialHTMLKeyboardResponse = {
  type: htmlKeyboardResponse,
  stimulus: `<p style="font-size: ${font_size}px">+</p>`,
  trial_duration: 500,
  choices: 'NO_KEYS',
};

const trial: TrialHTMLKeyboardResponse = {
  type: htmlKeyboardResponse,
  stimulus: function (): string {
    return `<p style="font-size: ${font_size}px">${getVarFlanker('stim')}</p>`;
  },
  choices: ['f', 'j'],
  data: {
    condition: getVarFlanker('condition'),
  },
  on_finish: function (data: DataHTMLKeyboardResponse): void {
    data.answer = getVarFlanker('answer');
    data.correct = Number(jsPsych.pluginAPI.compareKeys(data.response, data.answer));
  },
};

const flanker: Block = {
  timeline: [fixation, trial],
  timeline_variables: stimsFlankerMain,
  sample: {
    type: 'fixed-repetitions',
    size: 2,
  },
};

jsPsych.run([flanker]);

個人的な推しポイントはjsPsych.timelineVariable()の引数を限定できるように作成したラッパー関数getVarFlankerです。下記のコードを参照してください。説明の都合上,フランカー課題用のtimeline_variablesの型定義FlankerVariablesとそれを利用した変数stimsFlankerMainも併記しています。

type FlankerVariables = {
  stim: string;
  condition: '一致' | '不一致';
  answer: 'f' | 'j';
};

const stimsFlankerMain: FlankerVariables[] = [
  { stim: '<<<<<', condition: '一致', answer: 'f' },
  { stim: '>>>>>', condition: '一致', answer: 'j' },
  { stim: '>><>>', condition: '不一致', answer: 'f' },
  { stim: '<<><<', condition: '不一致', answer: 'j' },
];

const getVarFlanker = (varname: keyof FlankerVariables): string => jsPsych.timelineVariable(varname);

getVarFlankerはフランカー課題用のtimeline_variables(上のサンプルではstimsFlankerMain)の値を呼び出すために使用されます。この関数の引数には,stimsFlankerMainのプロパティ名('stim', 'condition', 'answer')しか指定できないようになっています。それら以外を指定するとエラーが発生します。

jsPsych.timelineVariable()に指定するプロパティ名をタイポや勘違いから間違ってしまっていて,実験がうまく走らないなんてことありませんか?私はあります。

timeline_variablesのプロパティ名を編集したあと,jsPsych.timelineVariable()の引数を変更し忘れていて実験が走らないなんてことありませんか?私はあります。

timelineVariable()にまつわるエラーは内容やコードの箇所が見つけにくいと思います。しかし,上記のような型定義をしておけば,実行前に検出することができ,五里霧中な修正作業に時間を割く必要がなくなります。今後重宝しそうです。

webpack でバンドル

TypeScript のコードは JavaScript に変換する必要があります。前回の記事では複数のモジュール・css をまとめるためにwebpackを利用しましたが,webpackは TS -> JS の変換も行ってくれます。便利ですね。

変換のために,webpack.config.jsを以下のように編集します。

webpack.config.js
  const path = require('path');

  module.exports = {
-   entry: './src/hello-world.js',
+   entry: './src/flanker.ts', // 上のファイル名に合わせる
    output: {
      filename: 'main.js',
      path: path.resolve(__dirname, 'dist'),
    },
    module: {
      rules: [
        {
          test: /\.css$/,
          use: ['style-loader', 'css-loader'],
        },
+       {
+         test: /\.ts?$/,
+         use: 'ts-loader',
+         exclude: /node_modules/,
+       },
      ],
    },
  };

編集後,以下のコマンドを実行するとdistディレクトリにmain.jsが生成されています。

npx webpack --mode development

実験用の html ファイル(前回の記事を参照)を開くとフランカー課題が実施されます。

おわりに

今回の記事では,TypeScript で jsPsych ベースの心理学実験を作成する方法を紹介しました。型を活用することで変数の仕様を明確にし,コーディング時の迷いを減らすことができます。また,仕様に沿っていない箇所はエラーとして認識されるため,コード上のミスに事前に気づくことができます。

実際の jsPsych 実験コードは今回のサンプルよりももっと長いはずです。コードが長くなればなるほどミスが発生する可能性は高くなり,さらに,ミスの箇所を見つけるのに必要な時間も長くなります。そのため,実際の実験プログラムを書く際に型定義を用いるメリットは本記事で紹介した以上に大きいと思われます。

もちろん,型を導入するためには(TypeScript における)型の仕様を理解している必要があります。このハードルは高いと思います。今回の記事では TypeScript でのコーディング例を示すことが目的だったので,型について解説することは避けました[1]。また機会があれば紹介したいと思います。

型の説明は端折っていますが,この記事をきっかけに 一人でも多くの人が TypeScript を用いて jsPsych ベースの実験作成に挑戦する人がいらっしゃれば大変幸いです。

不定期にはなりますが,今後も引き続き心理学実験・研究法に関する Tips を共有していきます。いいね・サポートをいただけると大変励みになりますので,ぜひそちらもよろしくお願いします。

脚注
  1. ちょっと自信がないというのも理由の一つです。 ↩︎

Discussion