簡単な偏差値計算Webアプリをさくっと作りながら TypeScript Modules を学習します。
module(変数、関数、クラス etc)は明示的に export で公開しなければ、外部に参照されません(グローバルスコープではない)。export した module を import することによって使用できます。
Modules import は、module loader を使用して相互に import します。module loader には CommonJS、RequireJS などがありますが、今回は ECMAScript 2015 で実装します。
環境
バージョン
- ASP.NET Core 2.2 MVC
- TypeScript 3.6.2 + javascript
- Chrome(dynamic import に対応している 63 以降)
範囲外
- TypeScript のインストール
- ASP.NET Core MVC サーバサイドの実装
Visual Studio を使って ASP.NET Core MVC プロジェクトに TypeScript を npm でインストールする方法は、別の機会に紹介したいと思います。
ビュー

10人の数学の点数をテキストボックスに入力して、計算ボタンをクリックすると「平均」「分散」「標準偏差」「偏差値」を計算して表示するようにします。
Index.cshtml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
<h2>数学のテストの点数</h2> <div class="d-flex bd-highlight"> <div class="p-2 flex-fill bd-highlight"> <div class="form-group"> <label>Daniel:</label> <input type="number" class="form-control" id="en-1" name="en-1" data-deviation="deviation-1" min="0" max="100" /> <label id="deviation-1"></label> </div> </div> <div class="p-2 flex-fill bd-highlight"> <div class="form-group"> <label>Ethan:</label> <input type="number" class="form-control" id="en-2" name="en-2" data-deviation="deviation-2" min="0" max="100" /> <label id="deviation-2"></label> </div> </div> <div class="p-2 flex-fill bd-highlight"> <div class="form-group"> <label>Emily:</label> <input type="number" class="form-control" id="en-3" name="en-3" data-deviation="deviation-3" min="0" max="100" /> <label id="deviation-3"></label> </div> </div> <div class="p-2 flex-fill bd-highlight"> <div class="form-group"> <label>Olivia:</label> <input type="number" class="form-control" id="en-4" name="en-4" data-deviation="deviation-4" min="0" max="100" /> <label id="deviation-4"></label> </div> </div> <div class="p-2 flex-fill bd-highlight"> <div class="form-group"> <label>Michael:</label> <input type="number" class="form-control" id="en-5" name="en-5" data-deviation="deviation-5" min="0" max="100" /> <label id="deviation-5"></label> </div> </div> </div> <div class="d-flex bd-highlight"> <div class="p-2 flex-fill bd-highlight"> <div class="form-group"> <label>James:</label> <input type="number" class="form-control" id="en-6" name="en-6" data-deviation="deviation-6" min="0" max="100" /> <label id="deviation-6"></label> </div> </div> <div class="p-2 flex-fill bd-highlight"> <div class="form-group"> <label>Anna:</label> <input type="number" class="form-control" id="en-7" name="en-7" data-deviation="deviation-7" min="0" max="100" /> <label id="deviation-7"></label> </div> </div> <div class="p-2 flex-fill bd-highlight"> <div class="form-group"> <label>Layla:</label> <input type="number" class="form-control" id="en-8" name="en-8" data-deviation="deviation-8" min="0" max="100" /> <label id="deviation-8"></label> </div> </div> <div class="p-2 flex-fill bd-highlight"> <div class="form-group"> <label>David:</label> <input type="number" class="form-control" id="en-9" name="en-9" data-deviation="deviation-9" min="0" max="100" /> <label id="deviation-9"></label> </div> </div> <div class="p-2 flex-fill bd-highlight"> <div class="form-group"> <label>John:</label> <input type="number" class="form-control" id="en-10" name="en-10" data-deviation="deviation-10" min="0" max="100" /> <label id="deviation-10"></label> </div> </div> </div> <div class="d-flex bd-highlight"> <div class="p-2 flex-fill bd-highlight"> <div class="form-group"> <label><strong>平均</strong></label> <label id="mean"></label> </div> </div> <div class="p-2 flex-fill bd-highlight"> <div class="form-group"> <label><strong>分散:</strong></label> <label id="variance"></label> </div> </div> <div class="p-2 flex-fill bd-highlight"> <div class="form-group"> <label><strong>標準偏差:</strong></label> <label id="standard"></label> </div> </div> </div> <input type="button" class="btn btn-primary" id="calc" value="計算" /> @section Scripts { <script type="module"> import('/js/Main.js') .then(module => { let main = new module.Main(); let btn = document.getElementById('calc'); btn.addEventListener('click', () => { main.execute(); }, { once: false, capture: false, passive: true }) }) .catch(error => { console.log(`error: ${error.message}`); }); </script> } |
module の動的インポート
import キーワードを関数として呼び出して、モジュールを動的にインポートします。非同期処理の最終的な完了処理、結果を Promise で返します。また、ECMAScript 2017 で導入された async/await でも実装できます。
Promise
fetch での非同期処理を紹介しています。fetch の戻り値も Promise です。
MDN でもの下記のページを下までスクロールするとブラウザ実装状況も確認できます。
アロー関数
addEventListener のイベントをアロー関数で定義しています。this は使ってないけど。
Promise、アロー関数 ともに ECMAScript 2015 の目玉となる機能です。
モジュール
module 間の関係は、ファイルレベルでの import、export になります。
クラス、インタフェース、Enum など、今回は学習も兼ねて、各1ファイルにしています。というのも、chrome developer tools のネットワークを見て、パフォーマンスどうなの?ということについても触れるためです。
Main.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
import StudentScoreStatistics from "./StudentScoreStatistics.js"; import Statistics from "./Statistics.js"; import Student from "./Student.js"; import Subject from "./Subject.js"; import { SubjectType } from "./SubjectType.js"; import StatisticsOneDimCalculator from "./StatisticsOneDimCalculator.js"; export class Main { constructor() { } public execute(): void { // 1. input type="number" の要素を取得します let elems = <NodeListOf<HTMLInputElement>>document.body.querySelectorAll("input[type=\"number\"]"); // 2. 科目を数学に設定します let subjectType: SubjectType = SubjectType.Math; // 3. 1 の要素数 = 生徒数 として Student を生成します let students: Student[] = this.createStudents(elems, subjectType); // 4. 計算します let result: StudentScoreStatistics = this.calculate(students, subjectType); // 5. 計算結果を設定します this.setCalculateResult(result, subjectType, students); } private createStudents(elements: NodeListOf<HTMLInputElement>, subjectType: SubjectType): Student[] { let students: Student[] = []; for (const elem of elements) { let id = elem.getAttribute("data-deviation"); students.push(new Student(id, [new Subject(subjectType, Number(elem.value))])); } return students; } private calculate(students: Student[], subjectType: SubjectType): StudentScoreStatistics { // StatisticsCalculatorImpl を DI します let result = new StudentScoreStatistics(students, new StatisticsOneDimCalculator()); result.calculate(subjectType); return result; } private setCalculateResult(statistics: Statistics, subjectType: SubjectType, students: Student[]): void { // 平均を設定します let mean = document.getElementById("mean"); mean.textContent = statistics.mean().toString(); // 分散を設定します let variance = document.getElementById("variance"); variance.textContent = statistics.var().toString(); // 標準偏差を設定します let standard = document.getElementById("standard"); standard.textContent = statistics.std().toString(); // 偏差値を設定します for (const student of students) { let devVal = student.getDevVal(subjectType, statistics.mean(), statistics.std()); let elem = document.getElementById(`${devVal[0]}`); elem.textContent = devVal[1].toString(); } } } |
import 宣言方法
ここでは、import **** from “ファイル名” と宣言しています。import は、どういう構造で module が export されているかによります。
他にも、関数単位、 * 、別名などの宣言方法があります。詳細は公式サイトをご覧ください。
export 宣言方法
ここでは、export class と宣言しています。他では、export default class で宣言しています。
Each module can optionally export a default export. Default exports are marked with the keyword default; and there can only be one default export per module. default exports are imported using a different import form
この説明だけで default を理解するのは難しいですが、以下の TypeScript ドキュメントにある JQuery の import サンプルをみれば分かりやすいと思います。
1 2 3 4 5 6 7 |
// JQuery.d.ts declare let $: JQuery; export default $; // App.ts import $ from "jquery"; $("button.continue").html( "Next Step..." ); |
今回は各クラスを1ファイルにしていますが、こんな export も可能です。
1 2 3 4 5 6 7 8 9 |
// MyLargeModule.ts export class Dog { ... } export class Cat { ... } export class Tree { ... } export class Flower { ... } // Consumer.ts import * as myLargeModule from "./MyLargeModule.ts"; let x = new myLargeModule.Dog(); |
Student.ts
Main クラスで生成した生徒クラスです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import Subject from "./Subject.js"; import { SubjectType } from "./SubjectType.js"; import StatisticsCalculatorImpl from "./StatisticsCalculatorImpl.js"; export default class Student { private _subjects: Subject[]; private _calc: StatisticsCalculatorImpl; private _id: string; constructor(id: string, subjects: Subject[]) { this._id = id; this._subjects = subjects; } set calc(calc: StatisticsCalculatorImpl) { this._calc = calc; } // 科目の点数を取得します public getScore(subjectType: SubjectType): number { return this._subjects.find(subject => subject.type === subjectType).score; } // 偏差値を取得します public getDevVal(subjectType: SubjectType, mean: number, std: number): [string, number] { let score = this._subjects.find(subject => subject.type === subjectType).score; return [this._id, this._calc.devVal(score, mean, std)]; } } |
ECMAScript 2015 から class が定義できるようになりました。ECMAScript 2015 以降も拡張が継続しています。
class
Subject.ts
生徒クラスのメンバーである科目クラスです。今回は数学ですね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import { SubjectType } from "./SubjectType.js"; export default class Subject { private _type: SubjectType; private _score: number; constructor(type: SubjectType, score: number) { this._type = type; this._score = score; } get type(): SubjectType { return this._type; } get score(): number { return this._score; } } |
SubjectType.ts
科目です。TypeScript では enum が使えます。
1 2 3 4 5 6 7 |
export enum SubjectType { English, Japanese, Math, Science, Society } |
Statistics.ts
統計インターフェースです。TypeScript では interface が使えます。
1 2 3 4 5 6 7 8 9 10 |
import { SubjectType } from "./SubjectType.js"; export default interface Statistics { calculate(subject: SubjectType): void; mean(): number; dev(): number[]; var(): number; std(): number; } |
StudentScoreStatistics.ts
Statistics インターフェースを実装したクラスです。implements ってあるから C# より Java よりの書き方かな。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
import Student from "./Student.js"; import Statistics from "./Statistics.js"; import StatisticsCalculatorImpl from "./StatisticsCalculatorImpl.js"; import { SubjectType } from "./SubjectType.js"; export default class StudentScoreStatistics implements Statistics { private _students: Student[]; private _calc: StatisticsCalculatorImpl; private _mean: number; private _dev: number[]; private _var: number; private _std: number; constructor(students: Student[], calc: StatisticsCalculatorImpl) { this._students = students; this._calc = calc; for (let students of this._students) { students.calc = this._calc; } } public calculate(subject: SubjectType): void { let scores: number[] = this.getScores(subject); this._mean = this._calc.mean(scores); this._dev = this._calc.dev(scores, this._mean); this._var = this._calc.var(this._dev, this._mean); this._std = this._calc.std(this._var); } private getScores(subject: SubjectType): number[] { let result: number[] = []; for (const student of this._students) { result.push(student.getScore(subject)); } return result; } public mean(): number { return this._mean; } public dev(): number[] { return this._dev; } public var(): number { return this._var; } public std(): number { return this._std; } } |
StatisticsCalculatorImpl.ts
計算インターフェースです。
1 2 3 4 5 6 7 |
export default interface StatisticsCalculatorImpl { mean(scores: number[]): number; dev(scores: number[], ave: number): number[]; var(devs: number[], ave: number): number; std(variance: number): number; devVal(score: number, mean: number, std: number): number; } |
StatisticsOneDimCalculator.ts
計算インターフェースを実装したクラスです。正直 static メソッドが適当だと思いましたが、インターフェースで実装したい衝動に負けクラスにして DI しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import StatisticsCalculatorImpl from "./StatisticsCalculatorImpl.js"; export default class StatisticsOneDimCalculator implements StatisticsCalculatorImpl { constructor() { } public mean(scores: number[]): number { let sum = scores.reduce((accumulator: number, currentValue: number) => accumulator + currentValue); return Math.floor(sum / scores.length); } public dev(scores: number[], ave: number): number[] { let result: number[] = []; for (const score of scores) { result.push(score - ave); } return result; } public var(devs: number[], ave: number): number { let sum: number = 0; for (const dev of devs) { sum += dev ** 2; } return Math.floor(sum / devs.length); } public std(variance: number): number { return Math.floor(Math.sqrt(variance)); } public devVal(score: number, mean: number, std: number): number { return Math.floor(((score - mean) / std) * 10 + 50); } } |
因みに偏差値の数式は以下です。
実行

python で計算した結果とあってるから問題なさそうです。
パフォーマンス

なるほど・・・ bundle しないとダメだな。
ということで、「【初心者向け】はじめての ASP.NET Core 作りながら学習」の方で、いつか以下を紹介したいと思います。
- TypeScript インストール
- TypeScript トランスパイル
- babel で es5 にトランスパイル
- webpack で bundle
Java、C# を主要言語としてきた僕としては、静的型付けとクラスベースオブジェクト指向の TypeScript は実装しやすく、DOM インターフェイスもより意識することができます。あえて TypeScript を選択肢から外すということはないかな。今回は ジェネリクス 等は使わなかったけど便利な機能がたくさんあるので色々試してみたいと思います。
↓ もうレガシーブラウザに振り回されないアプリを作成できます。