心理学実験作成で仲良くなるTypeScript
はじめに
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.js
やwebpack
の導入はすでに済んでいて,以下のようなディレクトリ構成であることを前提に話を進めます。この構成は,前回の記事で用いたものです。前回の記事にはNode.js
とwebpack
の導入についても書いてあります。それらの導入がまだの場合は参考にしてみてください。
.
├── dist
├── node_modules
├── package-lock.json
├── package.json
├── src
└── webpack.config.js
新たに TypeScript を利用するためにnpm
でtypescript
とts-loader
をインストールしてください。
npm install typescript ts-loader
typescript
は今回のテーマである TypeScript です。ts-loader
はwebpack
で TypeScript のコードをバンドルするために必要なモジュールです。
TypeScript 環境の設定を行います。以下のコマンドを実行すると設定ファイル tsconfig.json
が生成されます。
npx tsc --init
ファイル内を確認するとほとんどのオプションがコメントアウトされています。次の2点について変更します。
-
module
をes6
に変更(webpack 公式に準拠) -
moduleResolution
というオプションのコメントアウトを外す
有効な行だけを取り出すと,以下のようになります。ハイライトのかかっている部分はデフォルトから変更した部分です。
{
"compilerOptions": {
"target": "es5",
- "module": "CommonJS",
+ "module": "es6",
+ "moduleResolution": "node",
"esModuleInterop": true ,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}
実験を作成する
TypeScript を使って,実験を作成してみます。型定義をガシガシ使ってみたかったので,前回の記事のhello-world
よりも実践的なものとして,拙著チュートリアルのフランカー課題(の一部)を例として使用しています。
サンプルコード(100 行弱あるのでたたみました)
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
を以下のように編集します。
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 を共有していきます。いいね・サポートをいただけると大変励みになりますので,ぜひそちらもよろしくお願いします。
-
ちょっと自信がないというのも理由の一つです。 ↩︎
Discussion