20200223のJavaScriptに関する記事は19件です。

PlayCanvasでサンディちゃんを歩かせるゲームを作るぞ☆(第5回)

こんなん作ってます。

https://playcanv.as/b/iAPwWXqY/
※本解説よりも開発が進んでいることもございますのでご了承ください。

そういえばUIのことを考えていなかった・・・

ゲームのメイン部分を作っててメッセージとかステータスを表示するUI部分を忘れてた・・・てこと、よくありますよね。
私もよく後になってテキトーなものを作ることが多々あります。

PlayCanvasはJavaScriptベース・・・ということは、HTMLがそのまんま使えるんじゃね?

・・・

チュートリアルにありました。(説明はない)
https://developer.playcanvas.com/ja/tutorials/htmlcss-ui/

今回はほぼチュートリアルに沿う形で、かつ僕が使いやすいテンプレートになるように、HTMLでダイアログを作ってメッセージを表示するところまでやります。

表示したいHTMLを作成する

Assetsでuiディレクトリを作成し、その直下に「css」「message」を用意します。
スクリーンショット 2020-02-23 15.12.17.png

「css」はそのままの意味です。(わかんない人は置いていく主義・・・)

.container {
    height: 16vh;
    width: 25vw;

    background-color: #444;
    padding: 8px;

    color: #fff;
    font-size: 18px;
    font-weight: 100;
    border-radius: 12px;
    box-shadow: 0 0 16px rgba(0, 0, 0, .3);
}

.container > .button {
    float: right;
    display: inline-block;
    background-color: #07f;
    padding: 0 16px;
    font-size: 18px;
    line-height: 32px;
    border-radius: 4px;
    cursor: pointer;

    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -khtml-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
}

.container > .caption {
    background-color: #E85700;
    width :100%;
    //padding: 0 16px;
    font-size: 20px;
    line-height: 32px;
    border-radius: 0px;
}

.container > .message {
    //margin-top: 52px;
    padding: 0 16px;
    font-size: 18px;
    line-height: 32px;
    border-radius: 0px;
}

.pos_top_left {
    position: absolute;
    top: 0;
    left: 0;
    margin: auto;
}


.pos_top_center {
    position: absolute;
    left: 0;
    right: 0;
    margin: auto;
}

.pos_top_right {
    position: absolute;
    top: 0;
    right: 0;
    margin: auto;
}

.pos_center_left {
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    margin: auto;
}

.pos_center_center {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    margin: auto;
}

.pos_center_right {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    margin: auto;
}


.pos_bottom_left {
    position: absolute;
    left: 0;
    bottom: 0;
    margin: auto;
}

.pos_bottom_center {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    margin: auto;
}

.pos_bottom_right {
    position: absolute;
    right: 0;
    bottom: 0;
    margin: auto;
}

containerクラスはゲームからのメッセージを載せるダイアログの形状、pos_~のクラスは表示する位置(左上・中央上・右上・左中央・画面真ん中・右中央・左下・中央下・右下)を示します。

「message」は表示するHTMLを書きます。ヘッダ部などは今回は要りません。

<div class="container pos_center_center">
    <div class="caption">center</div>
    <div class="message">center</div>
    <div class="button">Close</div>
</div>

<div class="container pos_top_left">
    <div class="caption">top</div>
    <div class="message">left</div>
    <div class="button">Close</div>
</div>


<div class="container pos_top_center">
    <div class="caption">top</div>
    <div class="message">center</div>
    <div class="button">Close</div>
</div>

<div class="container pos_top_right">
    <div class="caption">top</div>
    <div class="message">right</div>
    <div class="button">Close</div>
</div>

<div class="container pos_center_left">
    <div class="caption">center</div>
    <div class="message">left</div>
    <div class="button">Close</div>
</div>

<div class="container pos_center_right">
    <div class="caption">center</div>
    <div class="message">right</div>
    <div class="button">Close</div>
</div>


<div class="container pos_bottom_left">
    <div class="caption">bottom</div>
    <div class="message">left</div>
    <div class="button">Close</div>
</div>

<div class="container pos_bottom_center">
    <div class="caption">bottom</div>
    <div class="message">center</div>
    <div class="button">Close</div>
</div>

<div class="container pos_bottom_right">
    <div class="caption">bottom</div>
    <div class="message">right</div>
    <div class="button">Close</div>
</div>


9つのダイアログを表示します。

スクリプトからHTMLを読み込んで表示

まずスクリプトを作成。
Assetsでsrcディレクトリ直下に「ui.js」を作成。
スクリプト属性で「css」「html」を作成。
(後ほどエディタ上から、cssにはAssets/ui上のcssを、htmlには同じくmessageを渡すようにします)

ui.js
var Ui = pc.createScript('ui');

Ui.attributes.add('css', {type: 'asset', assetType:'css', title: 'CSS Asset'});
Ui.attributes.add('html', {type: 'asset', assetType:'html', title: 'HTML Asset'});


// initialize code called once per entity
Ui.prototype.initialize = function() {
    // CSSを適用する
    var style = document.createElement('style');
    document.head.appendChild(style);
    style.innerHTML = this.css.resource || '';

    // DIVタグを作成し、messageファイルの中身を書き込む
    this.div = document.createElement('div');
    this.div.innerHTML = this.html.resource || '';

    // 画面に作成したDIVタグを載せる
    document.body.appendChild(this.div);

};

次にRoot直下に「ui」という名前で空のEntityを作成し、ADD COMPONENTでSCRIPTを追加し、ui.jsを使うように割り当てます。
そしてスクリプト属性「css」にアセットの「css」を、「html」にアセットの「html」を充てます。

Launchすると・・・

スクリーンショット 2020-02-23 15.55.12.png

はい、鬱陶しいほどダイアログが並んで出てきました!!

Closeボタンで閉じる

ui.js
// initialize code called once per entity
Ui.prototype.initialize = function() {
    ・・・
    //最後に追加
    this.bindEvents();
}

Ui.prototype.bindEvents = function() {
    var self = this;

    // get button element by class
    var button_list = this.div.querySelectorAll('.button');
    var container_list = document.querySelectorAll('.container');

    button_list.forEach(function(button) {
        // add event listener on `click`
        button.addEventListener('click', function() {
            container_list.forEach(function(value) {

                value.style.display = 'none';

            });
        }, false);
    });
};

DOM要素にイベントをバインドする関数を用意して、Initialize関数で呼び出しています。
「どれか一つでもCloseボタンを押したら全てが非表示になる」という仕様にしていますが、もちろん個別で消せるようにもできます。
今回はわかりやすく大雑把にやらせてもらいました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PlayCanvasでサンディちゃんを歩かせるゲームを作るぞ☆(第5回)〜CSSとHTMLでUI部品作成〜

こんなん作ってます。

https://playcanv.as/b/iAPwWXqY/
※本解説よりも開発が進んでいることもございますのでご了承ください。

そういえばUIのことを考えていなかった・・・

ゲームのメイン部分を作っててメッセージとかステータスを表示するUI部分を忘れてた・・・てこと、よくありますよね。
私もよく後になってテキトーなものを作ることが多々あります。

PlayCanvasはJavaScriptベース・・・ということは、HTMLがそのまんま使えるんじゃね?

・・・

チュートリアルにありました。(説明はない)
https://developer.playcanvas.com/ja/tutorials/htmlcss-ui/

今回はほぼチュートリアルに沿う形で、かつ僕が使いやすいテンプレートになるように、HTMLでダイアログを作ってメッセージを表示するところまでやります。
(とりあえずUI部品を画面全体に置いてみて)

表示したいHTMLを作成する

Assetsでuiディレクトリを作成し、その直下に「css」「message」を用意します。
スクリーンショット 2020-02-23 15.12.17.png

「css」はそのままの意味です。(わかんない人は置いていく主義・・・)

.container {
    height: 16vh;
    width: 25vw;

    background-color: #444;
    padding: 8px;

    color: #fff;
    font-size: 18px;
    font-weight: 100;
    border-radius: 12px;
    box-shadow: 0 0 16px rgba(0, 0, 0, .3);
}

.container > .button {
    float: right;
    display: inline-block;
    background-color: #07f;
    padding: 0 16px;
    font-size: 18px;
    line-height: 32px;
    border-radius: 4px;
    cursor: pointer;

    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -khtml-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
}

.container > .caption {
    background-color: #E85700;
    width :100%;
    //padding: 0 16px;
    font-size: 20px;
    line-height: 32px;
    border-radius: 0px;
}

.container > .message {
    //margin-top: 52px;
    padding: 0 16px;
    font-size: 18px;
    line-height: 32px;
    border-radius: 0px;
}

.pos_top_left {
    position: absolute;
    top: 0;
    left: 0;
    margin: auto;
}


.pos_top_center {
    position: absolute;
    left: 0;
    right: 0;
    margin: auto;
}

.pos_top_right {
    position: absolute;
    top: 0;
    right: 0;
    margin: auto;
}

.pos_center_left {
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    margin: auto;
}

.pos_center_center {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    margin: auto;
}

.pos_center_right {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    margin: auto;
}


.pos_bottom_left {
    position: absolute;
    left: 0;
    bottom: 0;
    margin: auto;
}

.pos_bottom_center {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    margin: auto;
}

.pos_bottom_right {
    position: absolute;
    right: 0;
    bottom: 0;
    margin: auto;
}

containerクラスはゲームからのメッセージを載せるダイアログの形状、pos_~のクラスは表示する位置(左上・中央上・右上・左中央・画面真ん中・右中央・左下・中央下・右下)を示します。

「message」は表示するHTMLを書きます。ヘッダ部などは今回は要りません。

<div class="container pos_center_center">
    <div class="caption">center</div>
    <div class="message">center</div>
    <div class="button">Close</div>
</div>

<div class="container pos_top_left">
    <div class="caption">top</div>
    <div class="message">left</div>
    <div class="button">Close</div>
</div>


<div class="container pos_top_center">
    <div class="caption">top</div>
    <div class="message">center</div>
    <div class="button">Close</div>
</div>

<div class="container pos_top_right">
    <div class="caption">top</div>
    <div class="message">right</div>
    <div class="button">Close</div>
</div>

<div class="container pos_center_left">
    <div class="caption">center</div>
    <div class="message">left</div>
    <div class="button">Close</div>
</div>

<div class="container pos_center_right">
    <div class="caption">center</div>
    <div class="message">right</div>
    <div class="button">Close</div>
</div>


<div class="container pos_bottom_left">
    <div class="caption">bottom</div>
    <div class="message">left</div>
    <div class="button">Close</div>
</div>

<div class="container pos_bottom_center">
    <div class="caption">bottom</div>
    <div class="message">center</div>
    <div class="button">Close</div>
</div>

<div class="container pos_bottom_right">
    <div class="caption">bottom</div>
    <div class="message">right</div>
    <div class="button">Close</div>
</div>


9つのダイアログを表示します。

スクリプトからHTMLを読み込んで表示

まずスクリプトを作成。
Assetsでsrcディレクトリ直下に「ui.js」を作成。
スクリプト属性で「css」「html」を作成。
(後ほどエディタ上から、cssにはAssets/ui上のcssを、htmlには同じくmessageを渡すようにします)

ui.js
var Ui = pc.createScript('ui');

Ui.attributes.add('css', {type: 'asset', assetType:'css', title: 'CSS Asset'});
Ui.attributes.add('html', {type: 'asset', assetType:'html', title: 'HTML Asset'});


// initialize code called once per entity
Ui.prototype.initialize = function() {
    // CSSを適用する
    var style = document.createElement('style');
    document.head.appendChild(style);
    style.innerHTML = this.css.resource || '';

    // DIVタグを作成し、messageファイルの中身を書き込む
    this.div = document.createElement('div');
    this.div.innerHTML = this.html.resource || '';

    // 画面に作成したDIVタグを載せる
    document.body.appendChild(this.div);

};

次にRoot直下に「ui」という名前で空のEntityを作成し、ADD COMPONENTでSCRIPTを追加し、ui.jsを使うように割り当てます。
そしてスクリプト属性「css」にアセットの「css」を、「html」にアセットの「html」を充てます。

Launchすると・・・

スクリーンショット 2020-02-23 15.55.12.png

はい、鬱陶しいほどダイアログが並んで出てきました!!

Closeボタンで閉じる

ui.js
// initialize code called once per entity
Ui.prototype.initialize = function() {
    ・・・
    //最後に追加
    this.bindEvents();
}

Ui.prototype.bindEvents = function() {
    var self = this;

    // get button element by class
    var button_list = this.div.querySelectorAll('.button');
    var container_list = document.querySelectorAll('.container');

    button_list.forEach(function(button) {
        // add event listener on `click`
        button.addEventListener('click', function() {
            container_list.forEach(function(value) {

                value.style.display = 'none';

            });
        }, false);
    });
};

DOM要素にイベントをバインドする関数を用意して、Initialize関数で呼び出しています。
「どれか一つでもCloseボタンを押したら全てが非表示になる」という仕様にしていますが、もちろん個別で消せるようにもできます。
今回はわかりやすく大雑把にやらせてもらいました。

・・・ただこのやり方よりも、vue.js使う方がよりスマートに作れそうな気がしてます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

test投稿

javascriptの記法

if文

if (条件式) {
    アクション;
}

for loop

for (変数定義; 条件式; 更新式) {
アクション;
}

for (let number=1;number<=100;number ++ ){
  console.log(number);
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

node + javascript で今日の日付を文字列に変換する、YMD形式で出力する例 #javascript #node

なにやら strftime 的な YMD 形式でうまく区切るメソッドはなさそう?

node


> new Date().toISOString().split('T')[0]
'2020-02-22'

> new Date().toDateString()
'Sat Feb 22 2020'

> new Date().toDateString()
'Sat Feb 22 2020'

> new Date().toGMTString()
'Sat, 22 Feb 2020 08:43:15 GMT'

> new Date().toISOString()
'2020-02-22T08:43:22.536Z'

> new Date().toJSON()
'2020-02-22T08:43:31.867Z'

> new Date().toLocaleDateString()
'2/22/2020'

> new Date().toLocaleTimeString()
'5:43:53 PM'

> new Date().toLocaleString()
'2/22/2020, 5:44:00 PM'

> new Date().toString()
'Sat Feb 22 2020 17:44:09 GMT+0900 (Japan Standard Time)'

> new Date().toTimeString()
'17:44:15 GMT+0900 (Japan Standard Time)'

> new Date().toUTCString()
'Sat, 22 Feb 2020 08:44:22 GMT'

こんな prototype の関数が見つかるが、採用されなかったのだろうか

Date.prototype.toLocaleFormat() - JavaScript | MDN

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/3001

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

jQueryでのmapの使い方

はじめに

今回jQueryを使っている際に、HTML要素の配列を作りたいと思ったときに、オブジェクトにしか配列を作れないと思って、調べてれるとHTMLにも「.map()」が使えるとわかったので備忘録として残します。

「.map()」とは?

「.map()」は、HTML要素や配列・オブジェクトなどに対して繰り返し処理を実行して新しい配列要素を返してくれます。

HTML要素での「.map()」の使い方

sumple.html.haml
%p docomo
%p au
%p softbank

上記のようなpタグが3つあり、このpタグに対して「.map()」の繰り返し処理を使ってみます。

sumple.js
var texts = $('p').map(function(index, element){

  return element.innerHTML;

});

console.log(texts);
console.log(texts[0]);

結果

['docomo', 'au', 'softbank']
docomo

上記のような結果が返ってきます。

余談で、特定の要素をクリックして、その要素にチェックボックスがあり、チェックされてるかどうかを検証する場合

sumple.js
$(document).on('click', '.checkboxの要素', function(){
  var array = $('.checkboxの要素').map(function(index, value){
    return $(this).val();
    // $(this)は、イベントを発火させた要素の取得
  });
});

みたいな感じで、$(this)の値で新しい配列を作成することも可能です。

配列・オブジェクトでの「map()」の使い方

  • 注意点として、引数で持たせている、indexとvalueの順番がHTMLを書く場合と逆になっています。
sumple.js
var array = ['docomo', 'au', 'softbank'];

var array = $.map(array, function(value, index){

  return value + 'キャリア';

});

console.log(array)

結果

['docomoキャリア', 'auキャリア', 'softbankキャリア']

mapを活用

上記のように配列にしまうととても便利です。
eachで処理を回したり、条件分岐にも使えたりするので、配列を作成する場合は積極的に使うべきかと思います。

まとめ

今回は特定の要素をクリックして、その要素のidを配列に組み替えて、dataをHTMLに返す作業をした際に、特定のイベントがクリックされるたびに

var array = [];
array.push(id);

みたいなことをしたんですが、イベントが発火するたびに[]の配列が作成されて、一つしかpushされなかったので、mapを使えることを知って助かりました。
わかりにくい点が多いかと思いますが、ご指摘等あればよろしくお願いします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React Native】アプリの画面を作ってみた ~待ち受け画面編~

0. はじめに

最近、自作アプリについて学んでいます。
この記事は、前回までの僕が書いた記事を元にしています。
最初からアプリを作りたい場合は、この記事をご覧になる前に、

【React Native】アプリ開発 〜プロジェクトのビルドから下準備まで〜
【React Native】アプリ開発 〜ディレクトリの構造化〜
【React Native】アプリ開発 〜React Navigationの環境構築〜
【React Native】アプリ開発 〜画面遷移に挑戦してみた〜

を参考にして、同じ状態まで揃えて頂きたいです。

自分で環境などが整っていて、画面の作り方の部分だけを参考にしたい場合は、

React.Component{}

StyleSheet.create()

の中だけを参考にしていただければ問題ないと思います。

1. 前回の状態

今回は、前回の続きから、実際にアプリの待ち受け画面( WelcomeScreen )を書いてみます。

前回は、React Navigation を使って複数のスクリーンのもとになる.jsファイル( WelcomeScreen.js, SettingScreen.js )だけを用意しました。ゆくゆくは、App.js の中でこれらの画面を切り替えようと画策しています。

そして、 WelcomeScreen.jsの記述は、

WelcomeScreeen.js
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';

class WelcomeScreen extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Text>Welcome!!</Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

export default WelcomeScreen;

となっていました。

画面は、

となっていました。
では、ここから再開します。

目標はこんな感じです。

これは友人がレイアウトしてくれた素敵なフラットデザインなのです。
おしゃれですね。
感謝しかない。

2. 構造化

大まかに構造を考えると、

-ベースのコンテナ
|
|---背景のコンテナ
|   |--上半分の薄緑色の背景
|   |--下半分の白色の背景
|
|---文字・アイコンのコンテナ
|   |--Welcome toとタイトルのbox
|   |--あいこんのbox
|   |--下のSTARTボタンのbox

みたいな感じになってますね。

これを、StyleSheet での装飾を最小限にして構造化してみると、

WelcomeScreen.js
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';

class WelcomeScreen extends React.Component {
  render() {
    return (
      <View style={styles.container}>

        <View style={[styles.backgroundContainer, StyleSheet.absoluteFillObject]}>
          <View style={[{flex:1}, styles.sampleBox]}>
          </View>
          <View style={[{flex:1}, styles.sampleBox]}>
          </View>
        </View>

        <View style={[styles.mainContainer]}>
          <View style={[{flex: 1}, styles.sampleBox]}>
            <View>
              <Text style={[styles.sampleText]}>Welcome to</Text>
            </View>
            <View>
              <Text style={[styles.sampleText]}>タイトル</Text>
            </View>
          </View>

          <View style={[{flex: 1,backgroundColor:'#aaa'}, styles.sampleBox]}>
            <View style={[styles.iconBox]}>
              <Text style={[styles.sampleText]}>あいこん</Text>
            </View>
          </View>

          <View style={[{flex: 1}, styles.sampleBox]}>
            <View>
              <Text style={[styles.sampleText]}>LET'S START</Text>
            </View>
          </View>
        </View>

      </View>
    );
  }
}

//StyleSheet--------------------------------

const styles = StyleSheet.create({

  container:{
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },

  sampleBox:{
    alignItems: 'center',
    justifyContent: 'center',
    borderColor: "gray",
    borderWidth: 1,
  },

  sampleText: {
      fontSize: 35,
      textAlign: 'center',
      color: 'grey',
      fontWeight: 'bold',
    },

});

export default WelcomeScreen;

こんな感じになりました。
この時のスマホ画面はこんな感じです。

ポイントをあげていきます。

point1. 'style='の中の書き方

 <View style={[{flex:1}, styles.sampleBox]}>

僕はこんな感じで書いています。外側に{ }を書いて、その中に[ ]を書きます。
[ ]の中の、
左側の{ }には、下のStyleSheetから持って来ずに、直書きするものを、
右側には下のStyleSheetから持って来るものを書きます。

もしかすると僕の我流かもしれないので、オススメなどありましたらご教示願います。

point2. flexの使い方

flexは同じコンテナ上の同じ階層にあるものの相対的な大きさを決めています。

現段階では、backgroundContainer も、mainContainer もその一段下の階層にある要素の大きさ(縦)の比を同じにしています。

point3. 存在しないスタイルも構造体の中に書ける。

<View style={[styles.mainContainer]}>

や、

<View style={[styles.iconBox]}>

など、下の StyleSheet で定義していないスタイルを含めていてもエラーを吐くことはありません。

「あとで、細かい大きさや色などを修正しようかな...」

などと考えている場合は、構造化の段階で暫定的に書いておいてもいいかもしれません。
というのも、return() の中にはコメントが書けません。
(書き方が分かる方は教えてください...)

ですので、スタイルの名前だけでも書いておけば、この構造体が何なのかが分かるため、コメントの代替として使えます。

point4. StyleSheet.absoluteFillObject

これは、背景を縦横目一杯に広げたい場合に使えます。
自分で下の StyleSheet でスタイルを定義しなくても使えるスタイルみたいです。
他にもいろいろ 'StyleSheet.hogehoge' みたいに引っ張って来れるのもありそうですね。

3. 装飾

編集中...

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

階層構造をもつTODOリストの作り方

はじめに

以前、階層構造をもつTODOリストをWebアプリケーションとして実装したので、その知見を共有します。

実際に作成したTODOリスト(OSSとして公開)


機能一覧

  • メモの新規作成
  • メモの編集(編集前の内容がテキストボックスに入力されている)
  • メモの削除
  • メモの追加(メモAの下にメモBを紐付け)
  • メモを一つ上の階層に移動
  • メモの一括削除
  • 新規作成や編集時のテキストボックスを、ボックス外の任意の場所をクリックすることで非表示(UX)
  • MemoモードとTaskモードの切り替え
  • (Taskモードのみ) メモ横のチェックボックスにチェックを入れると、非同期で打ち消し線を表示

解説

技術スタックは、Docker Compose、Java、PostgreSQLがメインです。

内部では基本的にCRUDしか行っておらず、DB側で再帰的なデータを保存しています。

Docker Compose

portsを定義して、ホストとゲストのポートをマッピングしています。

サービス名tomcatpostgresでお互い接続は可能なはずですが、ipv4も固定していました(理由は失念してしまいました)。

version: "3.7"
services:
  tomcat:
# When building from source code, uncomment following build
# and remove local image. e.g.) docker rmi resotto/tomcat:1.0
#    build: ap/
    image: resotto/tomcat:1.0
    container_name: tomcat
    tty: true
    ports:
     - "8888:8080"
    networks:
      app_net:
        ipv4_address: 172.16.1.3

  postgres:
# When building from source code, uncomment following build
# and remove local image. e.g.) docker rmi resotto/postgres:1.0
#    build: db/
    image: resotto/postgres:1.0
    container_name: postgres
    tty: true
    networks:
      app_net:
        ipv4_address: 172.16.1.2

networks:
  app_net:
    ipam:
      driver: default
      config:
       - subnet: "172.16.1.0/24"

DB

使い捨てアプリのため、postgresサービスのDockerfileから呼び出すstartup.sh内にスキーマを定義していました。

要素テーブルとその関連テーブルの2つを定義しました。

要素テーブルは(恥ずかしいのですが)テーブル名が残念なのと、typeカラムは数字で種類の情報を保持しているため、アンチパターンです。

element_typesテーブルを定義し、そちらへの外部キー制約をつけるのが良いです。

関連テーブルは、要素テーブルの主キーidの親と子で複合主キーになっています。

#!/bin/bash
service postgresql-9.6 start
psql -U postgres -c "create role uranus superuser login"
createdb -U postgres -O uranus uranusdb
psql -U uranus uranusdb -c \
"create table mst_element ( \
  id serial PRIMARY KEY, \
  type integer, \
  title text, \
  is_checked boolean, \
  is_root boolean, \
  create_date date, \
  update_date date \
);"

psql -U uranus uranusdb -c \
"create table mst_relation ( \
  parent_id integer, \
  child_id integer, \
  PRIMARY KEY (parent_id, child_id) \
);"

/bin/bash

フロントエンド

(画面の実装は採用する技術次第ですが)再帰的に表示する部分を独立して実装しました。

<!-- index.jsp -->
<!DOCTYPE html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <title>Uranus</title>
        <link rel="stylesheet" href="${f:url('/css/index.css')}">
        <noscript>
            <link rel="stylesheet" href="${f:url('/css/noscript.css')}">
        </noscript>
    </head>
    <body>
        <!-- header -->
        <h1>
            <c:choose>
                <c:when test="${mode eq 0}">
                    [ Memo ] /
                    <s:link href="task">Task</s:link>
                </c:when>
                <c:otherwise>
                    <s:link href="memo">Memo</s:link>
                    / [ Task ]
                </c:otherwise>
            </c:choose>
        </h1>
        <p>
            <!-- create button -->
            <input type="button" onclick="toggleCreateMode()" value="new" class="btn createButton">
            <!-- clear section -->
            <div id="clearButton">
                <s:form>
                    <input type="submit" name="clear" value="clear" class="btn">
                    <input type="hidden" name="mode" value="${mode}">
                </s:form>
            </div>
        </p>
        <!-- create block -->
        <div id="createBlock">
            <s:form>
                <textarea name="inputText" rows="3" cols="30"></textarea>
                <input type="submit" name="create" value="create" class="btn">
                <input type="hidden" name="mode" value="${mode}">
            </s:form>
        </div>
        <!-- main contents -->
        <ul>
            <c:forEach var="elm" items="${list}" varStatus="parentStatus">
                <c:if test="${mode eq 1}">
                    <c:if test="${elm.updateDate.toString() != date}">
                        <p>
                            <h2>
                                ${elm.updateDate}
                            </h2>
                            <c:set var="date" value="${elm.updateDate.toString()}" scope="request"></c:set>
                        </p>
                    </c:if>
                </c:if>
                <c:set var="child" value="${elm}" scope="request"></c:set>
                <c:import url="element.jsp"></c:import>
                <br>
            </c:forEach>
        </ul>
        <script type="text/javascript" src="${f:url('/js/index.js')}"></script>
    </body>
</html>
<!-- element.jsp -->
<li>
    <c:if test="${mode eq 1}">
        <input type="checkbox" id="checkbox_${child.id}" onclick="toggleCheckbox(this)" ${child.isChecked ? "checked" : ""}>
    </c:if>
    <p>
        <c:if test="${child.isChecked}"><del></c:if>
            ${child.title}
        <c:if test="${child.isChecked}"></del></c:if>
        &nbsp;
    </p>
    <p>
        <input type="button" onclick="toggleAddMode(this)" value="+" class="btn addButton">
    </p>
    <p>
        <input type="button" onclick="toggleEditMode(this)" value="edit" class="btn editButton">
    </p>
    <c:if test="${child.isRoot == false}">
        <s:form>
            <input type="submit" name="up" value="↑" class="btn">
            <input type="hidden" name="targetId" value="${child.id}">
            <input type="hidden" name="mode" value="${mode}">
        </s:form>
    </c:if>
    <s:form>
        <input type="submit" name="remove" value="-" class="btn">
        <input type="hidden" name="targetId" value="${child.id}">
        <input type="hidden" name="mode" value="${mode}">
    </s:form>
    <div class="addBlock">
        <s:form>
            <textarea name="addText" rows="3" cols="30"></textarea>
            <input type="submit" name="add" value="add" class="btn">
            <input type="hidden" name="targetId" value="${child.id}">
            <input type="hidden" name="mode" value="${mode}">
        </s:form>
    </div>
    <div class="editBlock">
        <s:form>
            <textarea name="editText" rows="3" cols="30">${child.title}</textarea>
            <input type="submit" name="update" value="update" class="btn">
            <input type="hidden" name="targetId" value="${child.id}">
            <input type="hidden" name="mode" value="${mode}">
        </s:form>
    </div>
    <ul>
        <c:if test="${child.children != null}">
            <c:forEach var="child" items="${child.children}" varStatus="childStatus">
                <c:set var="child" value="${child}" scope="request"></c:set>
                <c:import url="element.jsp"></c:import>
            </c:forEach>
        </c:if>
    </ul>
</li>

UIUX

(変数を定数っぽく書いたりと迷走してますが)非同期で打ち消し線を入れたり、他の領域をクリックすることによる非表示を実装しています。

// index.js
let createMode = false;
let addMode = false;
let editMode = false;

const toggleCreateMode = () => {
    createMode = !createMode;
    const CREATE_BLOCK = document.getElementById("createBlock");
    if (createMode) {
        CREATE_BLOCK.style.display = "block";
    } else {
        CREATE_BLOCK.style.display = "none";
    }
}

const toggleAddMode = (elm) => {
    const ID = elm.id.split("_")[1];
    addMode = !addMode;
    const ADD_BLOCK = document.getElementById("addBlock_" + ID);
    if (addMode) {
        ADD_BLOCK.style.display = "block";
    } else {
        ADD_BLOCK.style.display = "none";
    }
};

const toggleEditMode = (elm) => {
    const ID = elm.id.split("_")[1];
    editMode = !editMode;
    const EDIT_BLOCK = document.getElementById("editBlock_" + ID);
    if (editMode) {
        EDIT_BLOCK.style.display = "block";
    } else {
        EDIT_BLOCK.style.display = "none";
    }
};

const toggleCheckbox = (elm) => {
    const XHR = new XMLHttpRequest();
    const FD = new FormData();
    const ID = elm.id.split("_")[1];
    const URL = "http://" + location.host
        + location.pathname.match(/\/.+\//) + "toggleCheck";
    FD.append("targetId", ID);
    XHR.open("POST", URL);
    XHR.send(FD);
}

const setEventListener = (selector) => {
    const ELM = document.getElementById(selector);
    if (selector == "createBlock") {
        document.addEventListener('click', function(e) {
            if (!e.target.closest(".createButton")
                    && !e.target.closest("#createBlock")) {
                createMode = false;
                ELM.style.display = "none";
            }
        }, false)
        return;
    }
    const CLASSNAME = selector.split("_")[0];
    if (CLASSNAME == "addBlock") {
        document.addEventListener('click', function(e) {
            if (!e.target.closest(".addButton")
                    && !e.target.closest("#" + selector)) {
                addMode = false;
                ELM.style.display = "none";
            }
        }, false)
    } else {
        document.addEventListener('click', function(e) {
            if (!e.target.closest(".editButton")
                    && !e.target.closest("#" + selector)) {
                editMode = false;
                ELM.style.display = "none";
            }
        }, false)
    }
}

const setId = (str, settingListener) => {
    const LIST = document.getElementsByClassName(str);
    for (let i = 0; i < LIST.length; i++) {
        const ID = str + "_" + i;
        LIST[i].setAttribute("id", ID);
        if (settingListener) {
            setEventListener(ID);
        }
    }
}

const setIndex = () => {
    setEventListener("createBlock");
    setId("addButton", false);
    setId("addBlock", true);
    setId("editButton", false);
    setId("editBlock", true);
}

window.onload = function() {
    setIndex();
}

バックエンド

(Webフレームワークにもよりますが)リクエストを受け取って必要な(ロジック)サービスを呼び出して処理を行っていました。

要素の入れ替えロジックの実装が大変でした。

package com.uranus.service;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.Resource;

import org.apache.log4j.Logger;
import org.dbflute.cbean.result.ListResultBean;
import org.dbflute.exception.EntityAlreadyDeletedException;
import org.dbflute.optional.OptionalEntity;

import com.uranus.dbflute.exbhv.MstElementBhv;
import com.uranus.dbflute.exbhv.MstRelationBhv;
import com.uranus.dbflute.exentity.MstElement;
import com.uranus.dbflute.exentity.MstRelation;
import com.uranus.dto.ElementDto;
import com.uranus.dxo.ElementDxo;
import com.uranus.util.MstElementDateComparator;
import com.uranus.util.MstElementIdComparator;
import com.uranus.util.StringUtil;
import com.uranus.util.Type;

public class IndexService {

    @Resource
    protected MstElementBhv mstElementBhv;

    @Resource
    protected MstRelationBhv mstRelationBhv;

    /**
     * Parent ElementDto
     */
    private static ElementDto parent;

    /**
     * Log4j logger
     */
    public Logger logger = Logger.getLogger(IndexService.class);

    // -------------------------- public methods --------------------------

    /**
     * Setup ElementDto and return it.
     * @param  text title
     * @return      assembled ElementDto
     */
    public ElementDto assembleElementDto(String text, String mode) {
        ElementDto dto = new ElementDto();
        dto.type = Integer.parseInt(mode);
        dto.title = StringUtil.sanitize(text);
        LocalDate now = LocalDate.now();
        dto.createDate = now;
        dto.updateDate = now;
        return dto;
    }

    /**
     * Get root elements and their children (recursively) from database.
     * @param  type element type
     * @return      contents list if exists, otherwise {@code null}
     */
    public List<ElementDto> getList(int type) {
        List<Integer> rootIdList = getRootElementsId(type);
        if (rootIdList.size() == 0) return null;
        Map<Integer, ElementDto> elmMap = createElementMap(rootIdList);
        ListResultBean<MstRelation> rootRels = getRootRelation(rootIdList);
        rootRels.forEach(rel -> {
            ElementDto parentDto = getParent(elmMap.get(rel.getParentId()));
            setChildren(parentDto, rel.getChildId());
        });
        return createSortedList(elmMap, type);
    }

    /**
     * Create new element.
     * @param dto target ElementDto
     */
    public void createElement(ElementDto dto) {
        insertElement(dto, true);
    }

    /**
     * Add element.
     * @param parentId parent element id
     * @param childDto child ElementDto
     */
    public void addElement(int parentId, ElementDto childDto) {
        MstElement element = insertElement(childDto, false);
        insertRelation(parentId, element.getId());
    }

    /**
     * Update element.
     * @param targetId target element id
     * @param text     element text
     */
    public void updateElementText(int targetId, String text) {
        ElementDto dto = getElementDtoById(targetId);
        dto.title = text;
        dto.updateDate = LocalDate.now();
        MstElement element = ElementDxo.toElementEntity(dto);
        updateElement(element);
    }

    /**
     * Remove element and its relation (recursively) from database.
     * @param id target element id
     */
    public void removeElement(int id) {
        MstElement element = getElementEntityById(id);
        if (element == null) return;
        MstRelation upwardRel = getUpwardRelation(element.getId());
        if (upwardRel != null) deleteRelation(upwardRel);
        removeDownwardContents(element.getId());
    }

    /**
     * Toggle element's isChecked property.
     * @param id target element id
     */
    public void toggleElementCheck(int id) {
        MstElement element = getElementEntityById(id);
        if (element == null) return;
        boolean checked = element.getIsChecked();
        element.setIsChecked(!checked);
        updateElement(element);
    }

    /**
     * Exchange elements relation.
     * @param childId target element id to be promoted
     */
    public void exchangeElements(int childId) {
        if (getElementDtoById(childId).isRoot) return;
        int parentId = getUpwardRelation(childId).getParentId();
        Integer ancestorId = getAncestorId(parentId);
        rearrangeAncestorRelation(ancestorId, parentId, childId);
        ListResultBean<MstRelation> childRels = getRelationByParentId(childId);
        ListResultBean<MstRelation> parentRels = getRelationByParentId(parentId);
        rearrangeChildRelation(parentId, childId, childRels);
        rearrangeParentRelation(parentId, childId, parentRels);
        toggleIsRootProperty(childId, parentId);
    }

    /**
     * Delete all elements and relations with type.
     * @param type contents type
     */
    public void clear(int type) {
        List<Integer> rootElmsIdList = getRootElementsId(type);
        rootElmsIdList.forEach(rootElm -> {
            removeDownwardContents(rootElm);
        });
    }

    // -------------------------- private methods --------------------------

    /**
     * Get ancestor element id.
     * @param  parentId parent element id
     * @return          ancestor element id if exists, otherwise {@code null}
     */
    private Integer getAncestorId(int parentId) {
        MstRelation rel = getUpwardRelation(parentId);
        if (rel != null) return rel.getParentId();
        return null;
    }

    /**
     * Rearrange relation between ancestor element, parent element, and target
     * element.
     * @param ancestorId ancestor element id
     * @param parentId   parent element id
     * @param targetId   target element id to be promoted
     */
    private void rearrangeAncestorRelation(Integer ancestorId, int parentId, int targetId) {
        if (ancestorId != null) {
            deleteRelation(ancestorId, parentId);
            insertRelation(ancestorId, targetId);
        }
    }

    /**
     * Rearrange relation between parent element and target element.
     * @param parentId   parent element id
     * @param targetId   target element id to be promoted
     * @param parentRels parent element's downward relation
     */
    private void rearrangeParentRelation(int parentId, int targetId, ListResultBean<MstRelation> parentRels) {
        removeRelations(parentId, parentRels);
        createRelations(targetId, parentRels);
        insertRelation(targetId, parentId);
    }

    /**
     * Rearrange target element downward relation.
     * @param parentId  parent element id
     * @param targetId  target element id to be promoted
     * @param childRels target element's downward relation
     */
    private void rearrangeChildRelation(int parentId, int targetId, ListResultBean<MstRelation> childRels) {
        if (childRels.size() > 0) {
            removeRelations(targetId, childRels);
            createRelations(parentId, childRels);
        }
    }

    /**
     * Get downward relation by parent id.
     * @param  parentId parent element id
     * @return          relation list
     */
    private ListResultBean<MstRelation> getRelationByParentId(int parentId) {
        ListResultBean<MstRelation> rels = mstRelationBhv.selectList(cb -> {
            cb.query().setParentId_Equal(parentId);
        });
        return rels;
    }

    /**
     * Toggle element is_root property.
     * @param newParentId element id to be promoted
     * @param oldParentId element id to be demoted
     */
    private void toggleIsRootProperty(int newParentId, int oldParentId) {
        MstElement oldParent = getElementEntityById(oldParentId);
        if (oldParent.getIsRoot()) {
            oldParent.setIsRoot(false);
            updateElement(oldParent);
            MstElement newParent = getElementEntityById(newParentId);
            newParent.setIsRoot(true);
            updateElement(newParent);
        }
    }

    /**
     * Create relation with parentId and relation list's childId.
     * @param parentId parent element id
     * @param rels     relation list
     */
    private void createRelations(int parentId, ListResultBean<MstRelation> rels) {
        rels.forEach(rel -> {
            if (parentId != rel.getChildId()) {
                insertRelation(parentId, rel.getChildId());
            }
        });
    }

    /**
     * Remove relation with parentId and relation list's childId.
     * @param parentId
     * @param rels
     */
    private void removeRelations(int parentId, ListResultBean<MstRelation> rels) {
        rels.forEach(rel -> {
            deleteRelation(parentId, rel.getChildId());
        });
    }

    /**
     * Create map from root elements id.
     * @param  rootIdList root elements id
     * @return            map key:element id, value:ElementDto
     */
    private Map<Integer, ElementDto> createElementMap(List<Integer> rootIdList) {
        Map<Integer, ElementDto> elmMap = new HashMap<>();
        ListResultBean<MstElement> rootElms = getRootElement(rootIdList);
        rootElms.forEach(rootElm -> {
            elmMap.put(rootElm.getId(), ElementDxo.toElementDto(rootElm));
        });
        return elmMap;
    }

    /**
     * Create list from map.
     * @param  map  key:id, value:ElementDto
     * @param  type contents type
     * @return      ElementDto list
     */
    private List<ElementDto> createSortedList(Map<Integer, ElementDto> map, int type) {
        List<ElementDto> list = new ArrayList<>(map.values());
        if (type == Type.MEMO.getType()) {
            list.sort(new MstElementIdComparator());
        } else {
            list.sort(new MstElementDateComparator());
        }
        return list;
    }

    /**
     * Get root elements by root elements id.
     * @param  rootIdList root elements id list
     * @return            root elements list
     */
    private ListResultBean<MstElement> getRootElement(List<Integer> rootIdList) {
        ListResultBean<MstElement> rootElms = mstElementBhv.selectList(cb -> {
            cb.query().setId_InScope(rootIdList);
            cb.query().addOrderBy_Id_Asc();
        });
        return rootElms;
    }

    /**
     * Get root relation by root elements id.
     * @param  rootIdList root elements id list
     * @return            root relation list
     */
    private ListResultBean<MstRelation> getRootRelation(List<Integer> rootIdList) {
        ListResultBean<MstRelation> rootRels = mstRelationBhv.selectList(cb -> {
            cb.query().setParentId_InScope(rootIdList);
            cb.query().addOrderBy_ParentId_Asc();
        });
        logger.info("    Root relation list: " + rootRels);
        return rootRels;
    }

    /**
     * Delete downward relation and its element recursively from database.
     * @param parentId parent element id
     */
    private void removeDownwardContents(int parentId) {
        List<MstRelation> downwardRels = getAllRelation(parentId);
        downwardRels.forEach(rel -> {
            removeDownwardContents(rel.getChildId());
            deleteRelation(parentId, rel.getChildId());
        });
        MstElement elm = getElementEntityById(parentId);
        deleteElement(elm);
    }

    /**
     * Update element.
     * @param elm element entity
     */
    private void updateElement(MstElement elm) {
        mstElementBhv.update(elm);
        logger.info("    Element updated: " + elm);
    }

    /**
     * Delete element from database by element entity.
     * @param  elm element entity
     * @return     element entity after deletion.
     */
    private MstElement deleteElement(MstElement elm) {
        mstElementBhv.delete(elm);
        logger.info("    Element deleted: " + elm);
        return elm;
    }

    /**
     * Delete relation from database by parentId and childId.
     * @param  parentId parent element id.
     * @param  childId  child element id.
     * @return          relation entity after deletion.
     */
    private MstRelation deleteRelation(int parentId, int childId) {
        MstRelation rel = new MstRelation();
        rel.setParentId(parentId);
        rel.setChildId(childId);
        return deleteRelation(rel);
    }

    /**
     * Delete relation from database by relation entity.
     * @param  rel relation entity
     * @return     relation entity after deletion.
     */
    private MstRelation deleteRelation(MstRelation rel) {
        try {
            mstRelationBhv.delete(rel);
        } catch (EntityAlreadyDeletedException e) {}
        logger.info("    Relation deleted: " + rel);
        return rel;
    }

    /**
     * Get all relation by id.
     * @param  parentId parent element id
     * @return          relation entity list
     */
    private ListResultBean<MstRelation> getAllRelation(int parentId) {
        ListResultBean<MstRelation> rels = mstRelationBhv.selectList(cb -> {
            cb.query().setParentId_Equal(parentId);
            cb.query().addOrderBy_ChildId_Asc();
        });
        return rels;
    }

    /**
     * Insert Element into database.
     * @param  dto    target element DTO
     * @param  isRoot whether argument DTO is root or not
     * @return        element entity after insertion
     */
    private MstElement insertElement(ElementDto dto, boolean isRoot) {
        MstElement element = ElementDxo.toElementEntity(dto);
        element.setId(null);
        element.setIsRoot(isRoot);
        mstElementBhv.insert(element);
        logger.info("    Element created: " + element);
        return element;
    }

    /**
     * Insert Relation into database.
     * @param  parentId parent element id
     * @param  childId  child element id
     * @return          relation entity after insertion.
     */
    private MstRelation insertRelation(int parentId, int childId) {
        MstRelation relation = new MstRelation();
        relation.setParentId(parentId);
        relation.setChildId(childId);
        mstRelationBhv.insert(relation);
        logger.info("    Relation created: " + relation);
        return relation;
    }

    /**
     * Get element from database by id.
     * @param  id element id
     * @return    ElementDto if entity exists, {@code null} otherwise
     */
    private ElementDto getElementDtoById(int id) {
        OptionalEntity<MstElement> op = mstElementBhv.selectEntity(cb -> {
            cb.query().setId_Equal(id);
        });
        if (op.isPresent()) return ElementDxo.toElementDto(op.get());
        return null;
    }

    /**
     * Get element from database by id.
     * @param  id element id
     * @return    element entity if it exists, otherwise {@code null}
     */
    private MstElement getElementEntityById(int id) {
        OptionalEntity<MstElement> op = mstElementBhv.selectEntity(cb -> {
            cb.query().setId_Equal(id);
        });
        if (op.isPresent()) return op.get();
        return null;
    }

    /**
     * Get relation from database by id.
     * @param  childId child element id
     * @return         relation entity if it exists, {@code null} otherwise
     */
    private MstRelation getUpwardRelation(int childId) {
        OptionalEntity<MstRelation> op = mstRelationBhv.selectEntity(cb -> {
            cb.query().setChildId_Equal(childId);
        });
        if (op.isPresent()) return op.get();
        return null;
    }

    /**
     * If parent has children, set it recursively by id.
     * @param parentDto parent ElementDto
     * @param childId   child element id
     */
    private void setChildren(ElementDto parentDto, int childId) {
        if (parentDto == null) return;
        ElementDto childDto = getElementDtoById(childId);
        if (childDto == null) return;
        // If child also has children, set it recursively
        setChildrenRecursively(childDto, childId);
        parentDto.children.add(childDto);
    }

    /**
     * Get relation with childId and call setChildren if they exist
     * @param childDto child ElementDto
     * @param childId  child element id
     */
    private void setChildrenRecursively(ElementDto childDto, int childId) {
        ListResultBean<MstRelation> rels = mstRelationBhv.selectList(cb -> {
            cb.query().setParentId_Equal(childId);
            cb.query().addOrderBy_ChildId_Asc();
        });
        rels.forEach(rel -> {
            setChildren(childDto, rel.getChildId());
        });
    }

    /**
     * Get root elements from database, and return their id.
     * @param  mode contents mode
     * @return      root elements id list
     */
    private List<Integer> getRootElementsId(int type) {
        ListResultBean<MstElement> elms = getRootElements(type);
        List<Integer> idList = new ArrayList<>();
        elms.forEach(elm -> {
            idList.add(elm.getId());
        });
        logger.info("    Root id list: " + idList);
        return idList;
    }

    /**
     * Get root elements with element type.
     * @param  type element type
     * @return      Element entity list
     */
    private ListResultBean<MstElement> getRootElements(int type) {
        return mstElementBhv.selectList(cb -> {
            cb.query().setIsRoot_Equal(true);
            cb.query().setType_Equal(type);
            cb.query().addOrderBy_Id_Asc();
        });
    }

    /**
     * Return the same ElementDto until argument DTO has different id.
     * @param  dto ElementDto
     * @return     parent if argument DTO has the same id as that of it,
     *                 argument DTO otherwise.
     */
    private ElementDto getParent(ElementDto dto) {
        if (parent != null && dto.id == parent.id.intValue()) return parent;
        parent = dto;
        return parent;
    }
}

所感

使用するテーブルはたったの二つだけですが、一方でアプリケーションロジックの実装がかなり大変でした。

もし同じ仕様を実装するなら

  • ドメイン駆動設計(注:完全な好み)
  • Compositeパターン
  • 閉包テーブル

等を使ってみるといいかもしれません。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptでブレークポイントを張る方法【Chrome】

debuggerを埋める

以下のような感じ

    const sampleFnc = () => {
      const a = "a";
      debugger;
    };

Chromeの開発者コンソールを開いてプログラムを実行すると、勝手にdebuggerの部分で止まってくれます

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

javascript(node.js)のクエリーストリング(qs)を解説

javascript(node.js)では、受け取ったデータは実はそのままでは使えません。クライアントからは、クエリーテキストと呼ばれる形式で送られてくるので、それをエンコードしておかないといけないのです。

それを行っているのが、qsオブジェクトの「parse」です。qs.parse(string)により、受け取ったデータ(string)をエンコードし、それぞれのパラメーターの値を整理したオブジェクトに変換してくれます。後は、このオブジェクトから必須な値を取り出して利用するだけです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

謎解きが好きすぎて未経験でも2週間で謎解きサイトが作れた話

自己紹介

こんにちは、おこめなんと言う者です。
普段はFree Templateという団体で謎解きやボードゲームをつくっています。

2019年のクリスマスに謎解きサイトをつくったので、その時の経験を共有して
WEB謎制作のハードルを下げられたらと思って記事を書いています。

お時間ある方は『クリスマスとイタズラ好きの悪魔ちゃん』を解いてみてくれたらうれしいです。

ちなみにWEB謎制作でプログラミングの楽しさにめざめて
今は謎解きスマホアプリを制作中です!
作りおわったらまた記事を書くと思うのでよろしくお願いします。

対象の読者

web謎作ってみたいけどやり方わからない謎クラスタ
謎解きに興味あるエンジニア

謎解きに興味もってくれて、「作りたい!」って思った人はTwitterでDMくれたら全力で手伝わせていただきます。

環境/言語

環境:wordpress(Cocoon)
言語:html,css,javascript,jQuery

もともと謎解きのブログとして運用していたwordpressのサイト上で作っています。
Cocoonというテーマを使うと、編集画面からjavascriptを書けるので簡単にできます。

基本的に画面の見た目はhtml,cssというものを使って
「クリックしたら〇〇する」みたいな条件はjavascriptを使うとできます。
jQueryはjavascriptみたいなものだと思ってます。(間違っていたらごめんなさい)
書き方が違うだけであまりやってることは変わらないと思います。

具体的な例

ここからはネタバレになってしまうので、まだ解いていない人で解きたい方はこちらが終わってから読んでください。
難しいと思ったところだけピックアップして説明しています。

アドベントカレンダー

calenderDoor.gif

CSSでアドベントカレンダー作ったよー

の記事を参考にしました。
コピペでこんなにかっこいいカレンダーがつくれるのすごいですよね。

あまり難しいことはわからないので、雪を降らすのはあきらめました。
重要なのはカレンダーの扉が開くなので!

下記のコードは扉の一部です。
ざっくり説明すると
クリックしたときだけ特殊なCSSが効き扉が開いたように見える仕組みです。
疑似セレクタ?というらしいですが詳しくはわかりません。

カレンダーの扉
      <td class="cale">
        <div class="advent-calendar__item">
          <input class="box" type="checkbox" name="1" value="1" id="1">
          <label for="1">
            <div class="contents">
              <img src="画像のURL">
            </div>
            <div class="contents2">
              <span class="contentsText"></span>
            </div>
            <div class="door"><span>1</span></div>
          </label>
        </div>
      </td>

クリックで画像切り替え

boushi.gif

HAPPY HALLOWEENがMERRY CHRISTMASに切り替わる部分です。
こちらも難しいことはやってなくて、クリックするとCSSが切り替わります。
魔女の帽子を隠す→サンタ帽を出す
みたいなことをやっています。

帽子の切り替え
      <td class="cale">
        <div class="hidden_box">
            <label for="label1" class="boushi1">
            </label>
            <input type="checkbox" id="label1"/>
          <div class="hidden_show">
            H
          </div>
          <div class="hidden_show2">
             M
          </div>
        </div>
      </td>
帽子の切り替え
/*チェックは見えなくする*/
.hidden_box input {
    display: none;
}

/*中身を非表示にしておく*/
.hidden_box .hidden_show {
    height: auto;
    opacity: 1;
}
/*クリックで中身表示*/
.hidden_box input:checked ~ .hidden_show {
    height: 0;
    padding: 0;
    overflow: hidden;
    opacity: 0;
    transition: 0.8s;
}
/*中身を非表示にしておく*/
.hidden_box .hidden_show2 {
    height: 0;
    padding: 0;
    overflow: hidden;
    opacity: 0;
    transition: 0.8s;
}

/*クリックで中身表示*/
.hidden_box input:checked ~ .hidden_show2 {
    height: auto;
    opacity: 1;
}

ただし、謎の答えを入力するまではクリックしても反応しないようにしたかったので
既に謎の答えを入力して正解していたら、クラスを切り替えるみたいなことを下記でやっています。

帽子をクリックしたときの処理
//魔女のぼうし
$(".boushi1").click(function () {
    if (questionFlg2) {

        $(this).removeClass("boushi1");
        $(this).addClass("boushi2");

        $(this).next(".hidden_show").css('display', 'none');
        $(this).next(".hidden_show").removeClass("hidden_show");

        $(this).next().next().addClass("hidden_show3");
        $(this).next().next().removeClass("hidden_show2");
    }

});

questionFlg2が正解済みであることをチェックしている部分です。

蜘蛛の巣を動かす

kumo.gif

こちらが参考サイトです。

・ドラッグアンドドロップする
JavaScriptを使って要素をドラッグ&ドロップで移動

・枠に吸い込まれるような見た目をつくる
こちら

↑の二つのサイトをコピペして組み合わせると蜘蛛の巣をドラッグアンドドロップして赤枠にはめると雪の結晶になる演出ができます。

スマホ全機種に対応するのが難しくて枠にはめる判定値(どれくらい近くにきたらはまった判定にするか)を大きくとらざるを得なかったので、クリックしただけで蜘蛛の巣が枠にはまった機種もあると思います。ごめんなさい。

あとはスマホだとドラッグしにくかったと思います。PC用にサイトをつくってしまったのが今回の反省点の一つです。

反省点

レスポンシブ対応
PC用の見た目を先につくってしまったので、スマホの見た目に修正するのがつらかったです。 次回つくるときは、スマホの見た目からつくることにします。
以前の作業を忘れる
1日3~4時間ずつ作っていたのですが前日やった作業を思い出すのに時間がかかっていた気がします。 まとまった時間をとって一気に作った方がいいと思いました!
名前を適当につけすぎた
よくわからないフラグやクラス名が沢山あって過去の自分をぶんなぐりたくなりました
よくわからないけど使ってみ過ぎた
コピペを多用しすぎて、何やってるか分からないまま進めたので細かい修正が全くできなくて困りました。 少しは理解しつつ進めないとだめですね・・・
表示遅すぎ
勉強不足でいいプログラミングができていないのに加えて画像を多用しているのでサイトがめちゃくちゃ重くなってしまいました。 いらない処理を消したり、画像を軽くするともっと早くなると思います。

まとめ

プログラミング未経験でしたが、ググりまくってコピペしまくれば低クオリティだけどなんとか動かせるものは作れるということがわかりました。

謎クラの皆さんぜひWEB謎いっぱいつくって解かせてください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Three.jsの超基本

Three.jsを触ってみたので、自身が忘れないために基本的な部分を書き残します。

超基本2はこちら->Three.jsの超基本2

Three.jsって何?

Wikipediaより引用
three.jsは、ウェブブラウザ上でリアルタイムレンダリングによる3次元コンピュータグラフィックスを描画する、クロスブラウザ対応の軽量なJavaScriptライブラリ及びアプリケーションプログラミングインタフェースである。

要は、Three.jsを使えばwebブラウザ上で簡単に3DCGのコンテンツを作れるということです。

Three.jsの導入

以下のサイトからjsファイルをダウンロード。
threejs

CDNはこちらから
https://cdnjs.com/libraries/three.js/

Scene(ステージ)

物体を置くためのステージ。

(function() {
    var scene;
    scene = new THREE.Scene();
})();

add関数で物体をステージに追加する。

scene.add(box); // Mesh(物体)を引数で渡す

Mesh(物体)

物体そのもの。
引数にGeometry(形状)とMaterial(材質)を渡すことができる。

(function() {
  var box;
  box = new THREE.Mesh(
    // 物体の形状を設定(幅, 高さ, 奥行き)
    new THREE.BoxGeometry(50, 50, 50),
    // 物体の材質を設定(色の指定は、始めに「0x」をつける)
    new THREE.MeshLambertMaterial({ color: 0xff0000 }),
  );
})();

Geometry(形状)

【立方体を作る場合】

new THREE.BoxGeometry("", "高さ", "奥行き")

【球体を作る場合】

new THREE.SphereGeometry"半径", "経度分割数", "緯度分割数", "開始経度", "経線中心角", "開始緯度", "緯線中心角"

【円柱を作る場合】

new THREE.CylinderGeometry"上面の半径", "底面の半径", "高さ", "円周の分割数", "高さの分割数", "フタをしない場合->true", "フタをする場合->false"

Material(材質)

【陰がつかない均一な塗りつぶしを表現】

new THREE.MeshBasicMaterial({ color: 0xff0000 })

【影のある光沢感のないマットな質感を表現】

new THREE.MeshLambertMaterial({ color: 0xff0000 })

【影のある光沢感のあるマットな質感を表現】

new THREE.MeshPhongMaterial({ color: 0xff0000 })

Camera(カメラ)

ステージ上にカメラを設置する。
設置したカメラ越しに見える物体が、レンダラーを介して描画される。

(function() {
  var camera;
  // カメラの作成(画角, アスペクト比, 描画開始距離, 描画終了距離)
  camera = new THREE.PerspectiveCamera(45, width/ height, 1, 1000);
  // カメラの位置(X軸, Y軸, Z軸)
  camera.position.set(200, 100, 300);
  // 注視点の設定
  camera.lookAt(scene.position);
})();

Light(ライト)

物体を照らすライトをステージ上に設置する。

(function() {
  var light;
  // ライトの作成(色, 光の強さ)
  light = new THREE.DirectionalLight(0xffffff, 1);
  // ライトの位置(X軸, Y軸, Z軸)
  light.position.set(0, 100, 30);
  // ライトをステージに追加
  scene.add(light);
})();

ステージ全体に均等に光を当てたい場合は、「AmbientLight」を使用する。

(function() {
  var ambient;
  ambient = new THREE.AmbientLight(0x404040);
  scene.add(ambient);
})();

renderer(レンダラー)

設定したステージ、物体、カメラ、ライトなどをHTMLに結びつける。

// アンチエイリアスをtrueにすることで、物体のギザギザを目立たなくする
renderer = new THREE.WebGLRenderer({ antialias: true });
// レンダラーのサイズを調整
renderer.setSize(width, height);
// 背景色を指定
renderer.setClearColor(0xefefef);
// デバイスの解像度を指定
renderer.setPixelRatio(window.devicePixelRatio);
// HTML要素に紐付ける
document.getElementById('stage').appendChild(renderer.domElement);

Helper(ヘルパー)

グリッドや軸、ライトの位置などを表示してくれる。

var gridHelper;
var axisHelper;
var lightHelper;

// グリッドを表示
gridHelper = new THREE.GridHelper(全体のサイズ, 1マスのサイズ);
scene.add(gridHelper);
// 座標軸を表示
axisHelper = new THREE.AxisHelper(線の長さ);
scene.add(axisHelper);
// ライトの位置を表示
lightHelper = new THREE.DirectionalLightHelper(light,表示する大きさ);
scene.add(lightHelper);

コードを全てまとめると

(function() {

  var scene;
  var box;
  var light;
  var ambient;
  var camera;
  var gridHelper;
  var axisHelper;
  var lightHelper;
  var renderer;
  var width = 500;
  var height = 250;

  // ステージの作成
  scene = new THREE.Scene();

  // 物体の作成
  box = new THREE.Mesh(
    new THREE.BoxGeometry(50, 50, 50),
    new THREE.MeshLambertMaterial({ color: 0xff0000 }),
  );
  box.position.set(0, 40, 0);
  scene.add(box);


  // ライトの設定
  light = new THREE.DirectionalLight(0xffffff, 1);
  light.position.set(0, 100, 30);
  scene.add(light);

  ambient = new THREE.AmbientLight(0x404040);
  scene.add(ambient);


  // カメラの設定
  camera = new THREE.PerspectiveCamera(45, width/ height, 1, 1000);
  camera.position.set(200, 100, 300);
  camera.lookAt(scene.position);


  // ヘルパーの設定
  gridHelper = new THREE.GridHelper(300, 10);
  scene.add(gridHelper);
  axisHelper = new THREE.AxisHelper(1000);
  scene.add(axisHelper);
  lightHelper = new THREE.DirectionalLightHelper(light, 20);
  scene.add(lightHelper);


  // レンダラーの設定
  renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(width, height);
  renderer.setClearColor(0xefefef);
  renderer.setPixelRatio(window.devicePixelRatio);
  document.getElementById('stage').appendChild(renderer.domElement);

  // 描画する
  renderer.render(scene, camera);

})();



上記のコードを書くことで、以下の3D立方体を作成することができます。
スクリーンショット 2020-02-23 14.59.39.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TypeScript 3.8 の発表

Announcing TypeScript 3.8 の日本語訳です。


TypeScript 3.8 は多くの新機能をもたらし、中には新しいものや、まもなく公開される ECMAScript 標準機能、型に限定した import/export のための新しいシンタックス、そしてさらに多くのものがあります。

Index

Type-Only Imports and Exports

この機能は、ほとんどのユーザーは考える必要はないかもしれません。しかし、もしあなたがここでの問題に直面していたら、それは興味の対象かもしれません(特に --isolatedModulestranspileModule API、Babel の下でのコンパイル時)。

TypeScript は JavaScript の import シンタックスを再利用し、我々は型を参照することができます。例えば次の例では、JavaScript の値である doThing を、純粋な TypeScript の型である Options と共に import できます。

// ./foo.ts
interface Options {
    // ...
}

export function doThing(options: Options) {
    // ...
}

// ./bar.ts
import { doThing, Options } from "./foo.js";

function doThingBetter(options: Options) {
    // do something twice as good
    doThing(options);
    doThing(options);
}

これは便利です。なぜなら、import する時のほとんどは、何が import されたかを心配する必要がないからです。ただ、何かを import しているというだけです。

残念ながら、これは import elision と呼ばれる機能のために機能していただけでした。TypeScript が JavaScript ファイルを出力する時、Optionsが型としてのみ使用されるのを確認し、自動的にその import を削除します。その結果の出力は、このように見えるでしょう。

// ./foo.js
export function doThing(options: Options) {
    // ...
}

// ./bar.js
import { doThing } from "./foo.js";

function doThingBetter(options: Options) {
    // do something twice as good
    doThing(options);
    doThing(options);
}

改めて、この振る舞いは通常は素晴らしいですが、幾らか他の問題を引き起こします。

まず最初に、値や型が export されているかどうかが曖昧ないくつかの場所があります。例えば、次の例におけるMyThingは、値と型のどちらでしょうか。

import { MyThing } from "./some-module.js";

export { MyThing };

ちょうどこのファイルだけに限定すれば、知る由もありません。Babel と TypeSrcipt の transpileModule API の両方は、もし MyThing が型のみであるならば正しく機能しないコードを生成するでしょうし、TypeScript の isolatedModule フラグはそれが問題になることを警告するでしょう。本当の問題は、"いやいや、本当は私はちょうど型を指したんだよ、つまりこれは削除されるべきだ"と主張する方法がないことであり、そのために import elision はいまいちなのです。

他の問題は、TypeScript import elision が、型として使われた import だけが含まれた import 文を取り除くつもりであることでした。それは副作用を持つモジュール郡に対して目立った異なる振る舞いを引き起こし、そのためにユーザーは副作用を完璧に保証する二つ目の import 文を挿入しなければならなかったでしょう。

// This statement will get erased because of import elision.
import { SomeTypeFoo, SomeOtherTypeBar } from "./module-with-side-effects";

// This statement always sticks around.
import "./module-with-side-effects";

これが現れるのを見た具体的な場所は、Angular.js (1.x)のようなフレームワーク内であり、(副作用である)service がグローバルに登録される必要があり、しかしそれらの service は型に対してただ import されたものでした。

// ./service.ts
export class Service {
    // ...
}
register("globalServiceId", Service);

// ./consumer.ts
import { Service } from "./service.js";

inject("globalServiceId", function (service: Service) {
    // do stuff with Service
});

結果として、./service.jsは、一度も実行されることはなく、実行時に壊れるでしょう。

この類の問題を避けるために、種々のものがどのようにインポート/削除されていたかに関する、よりきめの細かい制御をユーザーに提供すべきだと気付きました。

TypeScript 3.8 における解決策として、型だけの import と export に対する新しいシンタックスを追加しました。

import type { SomeThing } from "./some-module.js";

export type { SomeThing };

import typeは、型アノテーションと宣言に対して使用される宣言だけをインポートします。それはいつも完全に削除された状態で、よって実行時にはそれの残存物はありません。同じように、export typeは型コンテキストに対して使用されうる export を提供するだけで、それもまた TypeScript の出力からは削除されます。

実行時に値を、設計時に型をクラスはもち、その利用はコンテキストの影響を受けることに注意してください。クラスをインポートするためにimport typeを使用する時、それから継承するようなことはできません。

import type { Component } from "react";

interface ButtonProps {
    // ...
}

class Button extends Component<ButtonProps> {
    //               ~~~~~~~~~
    // error! 'Component' only refers to a type, but is being used as a value here.

    // ...
}

もし以前のフローを使用してきたなら、そのシンタックスはかなり似ています。一つの違いは、不明瞭に見えたかもしれないコードを避けるために少しの制限を追加したことです。

// Is only 'Foo' a type? Or every declaration in the import?
// We just give an error because it's not clear.

import type Foo, { Bar, Baz } from "some-module";
//     ~~~~~~~~~~~~~~~~~~~~~~
// error! A type-only import can specify a default import or named bindings, but not both.

import typeと共に、実行時に利用されないであろう import に伴って発生することを制御するための、新しいコンパイラのフラグ importsNotUsedAsValues も追加しました。このフラグは 3 つの異なる値を取ります

  • remove:これはこれらの import を削除する現在の振る舞いです。デフォルトであり続けることになっており、破壊的変更ではありません。
  • preserve:これは一度も使われていない値を持つ全ての import を保存します。これは、import/副作用を保存させることができます。
  • error:これは全ての import を保存します(preserveオプションと全く同じです)が、値の import が型としてのみ使用されたときにエラーとなるでしょう。どの値も思わず import されていないことを保証したい時にこれは便利だったかもしれませんが、import に明示的な副作用を今もなおもたらします。

この機能についての更なる情報は、pull requestや、import type宣言からの import が使用されうる場所の周辺での関連した変更から確認できます。

ECMAScript Private Fields

TypeScript 3.8 は、stage-3 class fields proposalの一部である、ECMAScript の private フィールドに対するサポートをもたらします。この仕事は、Bloomberg での良き友達によって開始され、完成まで突き動かされました。

class Person {
    #name: string

    constructor(name: string) {
        this.#name = name;
    }

    greet() {
        console.log(`Hello, my name is ${this.#name}!`);
    }
}

let jeremy = new Person("Jeremy Bearimy");

jeremy.#name
//     ~~~~~
// Property '#name' is not accessible outside class 'Person'
// because it has a private identifier.

通常のプロパティとは(private修飾子と共に宣言されるものでさえ)違って、private フィールドは覚えておくべき少しのルールを持っています。それらのいくつかは:

  • private フィールドは#文字から始まります。時々、これらを private names と呼びます。
  • 全ての private フィールド名は、それを含むクラスの領域で一意です。
  • publicあるいはprivateのような TypeScript のアクセス修飾子は、private フィールド上では使用できません。
  • private フィールドは、それが含まれるクラスの外側では(JS ユーザーによってでさえも)アクセスあるいは発見さえできません。時々、これを hard privacy と呼びます。

"hard" privacy から離れて、private フィールドのもう一つの利点は、ちょうど言及した一意性です。例えば、通常のプロパティ宣言は、サブクラスにおいて上書きされる傾向があります。

class C {
    foo = 10;

    cHelper() {
        return this.foo;
    }
}

class D extends C {
    foo = 20;

    dHelper() {
        return this.foo;
    }
}

let instance = new D();
// 'this.foo' refers to the same property on each instance.
console.log(instance.cHelper()); // prints '20'
console.log(instance.dHelper()); // prints '20'

private フィールドに伴い、これについて心配する必要はなく、というのもそれぞれのフィールド名はそのクラスで一意であるためです。

class C {
    #foo = 10;

    cHelper() {
        return this.#foo;
    }
}

class D extends C {
    #foo = 20;

    dHelper() {
        return this.#foo;
    }
}

let instance = new D();
// 'this.#foo' refers to a different field within each class.
console.log(instance.cHelper()); // prints '10'
console.log(instance.dHelper()); // prints '20'

なきに等しいもう一つのことは、その他の型で private フィールドにアクセスすると、TypeError をもたらすであろうことです!

class Square {
    #sideLength: number;

    constructor(sideLength: number) {
        this.#sideLength = sideLength;
    }

    equals(other: any) {
        return this.#sideLength === other.#sideLength;
    }
}

const a = new Square(100);
const b = { sideLength: 100 };

// Boom!
// TypeError: attempted to get private field on non-instance
// This fails because 'b' is not an instance of 'Square'.
console.log(a.equals(b));

最後に、いかなる単純な.jsファイルユーザーに対し、private フィールドは常にそれらが割り当てられる前に宣言されなければなりません。

class C {
    // No declaration for '#foo'
    // :(

    constructor(foo: number) {
        // SyntaxError!
        // '#foo' needs to be declared before writing to it.
        this.#foo = foo;
    }
}

TypeScript はクラスのプロパティに対していつも宣言を要求してきたのに対して、JavaScript はいつも、宣言されていないプロパティにユーザーがアクセスすることを許してきました。private フィールドに伴い、.jsあるいは.tsファイルのどちらに取り組んでいるかに関係なく、宣言が常に要求されます。

class C {
    /** @type {number} */
    #foo;

    constructor(foo: number) {
        // This works.
        this.#foo = foo;
    }
}

この実装についての更なる情報は、元の pull requestから確認できます。

Which should I use?

TypeScript ユーザーとして private のどちらの種類を使用すべきかについてのたくさんの質問を受け取ってきました。つまり一般的には、"privateキーワードを使うべきか、あるいは ECMAScript の hash/pound (#) private フィールドを使うべきか"です。

全ての良い質問に対し、その回答は良いものではありません。それは場合によるのです!

プロパティに関しては、TypeScript の private 修飾子は常に削除されます(すなわち、実行時にそれは全体的に普通のプロパティのように振る舞い、そしてそれが private 修飾子によって宣言されたことを伝える手段はありません)。private キーワードを使用する時、privacy はコンパイル時/設計時においてのみ強制され、JavaScript コードを使用する者に対してそれは全体的に意図によるものです。

class C {
    private foo = 10;
}

// This is an error at compile time,
// but when TypeScript outputs .js files,
// it'll run fine and print '10'.
console.log(new C().foo);    // prints '10'
//                  ~~~
// error! Property 'foo' is private and only accessible within class 'C'.

// TypeScript allows this at compile-time
// as a "work-around" to avoid the error.
console.log(new C()["foo"]); // prints '10'

上側は、"soft privacy"のこの類が、幾らかの API に対するアクセスを持たないことをコードの使用者が一時的に回避する手助けをし、そしてまた、どんな実行時においても機能します。

一方で、ECMAScript の#private は、そのクラスの外側からは完全にアクセス不可能です。

class C {
    #foo = 10;
}

console.log(new C().#foo); // SyntaxError
//                  ~~~~
// TypeScript reports an error *and*
// this won't work at runtime!

console.log(new C()["#foo"]); // prints undefined
//          ~~~~~~~~~~~~~~~
// TypeScript reports an error under 'noImplicitAny',
// and this prints 'undefined'.

この hard privacy は、内部のいかなるものを誰も利用できないことを厳密に保証する上では本当に便利です。もしライブラリの作者なら、private フィールドの削除あるいは改名は、決して破壊的変更をもたらすべきではありません。

上述したように、もう一つの利点は、ECMAScript の#privates に伴ってサブクラス化が容易であることであり、なぜならそれらは本当にprivate であるからです。ECMAScript#private フィールドを使用する時、どのサブクラスも今までフィールド名の衝突を心配したことはありません。TypeScript の private プロパティ宣言に関しては、ユーザーは今もなおスーパークラスで宣言されたプロパティを再び宣言しないように気をつけなければなりません。

考慮すべきもう一つの事項は、コードをどこで実行するつもりなのかです。TypeScript は現在、ECMAScript 2015 (ES6)あるいはそれ以上の target を標的としない限り、この機能をサポートできません。これは、privacy を強制するために基準を下げた実装が WeakMap を使用しており、WeakMap はメモリリークを引き起こさない方法では polyfill され得ないからです。対照的に、TypeScript の private 宣言されたプロパティは、全ての target で(ECMAScript 3 でさえ)機能します。

最後の考慮は速さだったかもしれません。つまり、private プロパティは他のプロパティと違いがなく、そのためそれらへのアクセスは、どの runtime を target としていようとも、他のプロパティアクセスと同じくらい速いです。対照的に、#private フィールドは WeakMap を使って基準が下げられているため、使用するにはより遅いかもしれません。幾らかの runtime は#private フィールドの実際の実装を最適化し、速度の速い WeakMap の実装さえ持っていたかもしれない一方で、全ての runtime ではそうでなかったかもしれません。

export * as ns Syntax

単一のメンバーとしてもう一つのモジュールの全てのメンバーを公開するような、単一のエントリーポイントを持つことは、しばしば一般的です。

import * as utilities from "./utilities.js";
export { utilities };

これはとても一般的なので、ECMAScript 2020 は最近、このパターンをサポートする新しいシンタックスを追加しました。

export * as utilities from "./utilities.js";

これは JavaScript に対して良い QOL の改善であり、そして TypeScript 3.8 はこのシンタックスを実装しています。モジュールの target がes2020よりも以前であるとき、TypeScript は最初のコードスニペットに沿って何かしらを出力するでしょう。

この機能を実装したコミュニティメンバーであるWenlu Wang (Kingwl)氏には特に感謝いたします。更なる情報は、元の pull requestをご確認ください。

Top-Level await

JavaScript の中で(HTTP リクエストのような)I/O を提供するほとんどのモダンな環境は非同期であり、多くのモダンな API は Promise を返します。これはノンブロッキングな操作を作る上で多くの利点を持つ一方で、ファイルあるいは外部のコンテンツのロードのようなものを驚くほど面倒にします。

fetch("...")
    .then(response => response.text())
    .then(greeting => { console.log(greeting) });

Promise に伴う.thenチェインを避けるため、JavaScript ユーザーはawaitを使用するためにasyncfunction をしばしば導入し、そしてそれを定義した後でその function をすぐさま呼んでいました。

async function main() {
    const response = await fetch("...");
    const greeting = await response.text();
    console.log(greeting);
}

main()
    .catch(e => console.error(e))

asyncfunction の導入を避けるため、近く発表される ECMAScript の使いやすい機能、"top-level await"を使用することができます。

以前は JavaScript において(似た機能を伴う他のほとんどの言語に沿って)、awaitasyncfunction の本文中でのみ許可されていました。しかし、top-level await によって、awaitをモジュールのトップレベルで使用することができます。

const response = await fetch("...");
const greeting = await response.text();
console.log(greeting);

// Make sure we're a module
export {};

繊細さがあることの注記:top-level await はモジュールのトップレベルでのみ機能し、TypeScript がimportあるいはexportを見つけるとき、ファイルはモジュールとしてのみ見なされます。幾らかの基本的なケースにおいて、これを保証するためにexport {}を幾らかの定型として書き出す必要があったかもしれません。

top level await は、現時点であなたが期待する全ての環境では機能しないかもしれません。現在、target コンパイラオプションがes2017かそれ以上で、moduleexnextあるいはsystemであるときのみ、top level await を使用できます。いくつかの環境とバンドラーの中でのサポートは、制限されるかもしれず、あるいは実験的サポートを有効にすることを要求するかもしれません。

実装の更なる情報は、元の pull requestをご確認ください。

es2020 for target and module

Kagami Sascha Rosylight (saschanaz)のおかげで、TypeScript 3.8 はes2020moduletargetに対するオプションとしてサポートします。これは、optional chaining, nullish coalescing, export * as ns, そして動的import(...)シンタックスのようなより新しい ECMAScript 2020 の機能を保存するでしょう。それはまた、bigintリテラルが今やexnext配下で安定した target を持つことを意味します。

JSDoc Property Modifiers

TypeScript 3.8 はallowJsフラグをオンにすることで JavaScript ファイルをサポートし、またcheckJsオプションか// @ts-checkコメントを.jsファイルのトップに追加することによって、それらの JavaScript ファイルのtype-checkingをサポートします。

JavaScript ファイルは型チェックに対する熱心なシンタックスを持たないために、TypeScript は JSDoc を利用します。TypeScript 3.8 はプロパティに対する少しばかりの新しい JSDoc タグを理解します。

最初はアクセス修飾子です。つまり@public, @private, @protectedです。これらのタグは TypeScript 内でそれぞれ機能するpublic, private, protectedと全く同じように機能します。

// @ts-check

class Foo {
    constructor() {
        /** @private */
        this.stuff = 100;
    }

    printStuff() {
        console.log(this.stuff);
    }
}

new Foo().stuff;
//        ~~~~~
// error! Property 'stuff' is private and only accessible within class 'Foo'.
  • @publicはいつも暗に意味され、省略できますが、プロパティがどこからでも参照されることを意味します。
  • @privateは、プロパティがそれを含むクラス内でしか利用できないことを意味します。
  • @protectedは、プロパティがそれを含むクラスと全ての生成されたサブクラス内で利用でき、それを含むクラスの似ていないインスタンス上では利用できないことを意味します。

次に、プロパティが初期化中にのみ書かれることを保証するための修飾子@readonlyを追加しました。

// @ts-check

class Foo {
    constructor() {
        /** @readonly */
        this.stuff = 100;
    }

    writeToStuff() {
        this.stuff = 200;
        //   ~~~~~
        // Cannot assign to 'stuff' because it is a read-only property.
    }
}

new Foo().stuff++;
//        ~~~~~
// Cannot assign to 'stuff' because it is a read-only property.

Better Directory Watching on Linux and watchOptions

TypeScript 3.8 はディレクトリのウォッチに対する(node_modulesに対する変更を効率的にピックアップするのに重要な)新しい戦略を生み出します。

幾らかのコンテキストに対して、Linux のようなオペレーティングシステム上では、TypeScript は依存性における変更を発見するために、node_modules上に(ファイルウォッチャーとは対照的な)ディレクトリウォッチャーとそのサブディレクトリの多くをインストールします。これは、より少ないディレクトリを追跡するための方法が存在する一方で、利用可能なファイルウォッチャーの数がしばしばnode_modules内のファイルによって覆い隠されるためです。

TypeScript のより古いバージョンは、フォルダー上にディレクトリウォッチャーをすぐさまインストールしたでしょうし、起動時はうまくいっていたでしょう。しかし、npm インストールの間、多くの処理がnode_modules内で実行され、それは TypeScript を圧倒しうることもあり、しばしばエディターのセッションを鈍くします。これを防ぐため、これらのかなり不安定なディレクトリが安定するための幾らかの時間を提供するために、TypeScript 3.8 はディレクトリウォッチャーのインストールまでわずかに待ちます。

全てのプロジェクトが異なる戦略下でよりうまく機能したかもしれず、そしてこの新しいアプローチがあなたのワークフローに対して機能しないであろうことを理由に、ユーザーが戦略をウォッチしているコンパイラ/言語サービスがファイルとディレクトリの経過を追うために使用されるべきであることを伝えられる新しいwatchOptionsフィールドを、TypeScript 3.8 はtsconfig.jsonjsconfig.jsonに導入します。

{
    // Some typical compiler options
    "compilerOptions": {
        "target": "es2020",
        "moduleResolution": "node",
        // ...
    },

    // NEW: Options for file/directory watching
    "watchOptions": {
        // Use native file system events for files and directories
        "watchFile": "useFsEvents",
        "watchDirectory": "useFsEvents",

        // Poll files for updates more frequently
        // when they're updated a lot.
        "fallbackPolling": "dynamicPriority"
    }
}

watchOptionsは設定できる 4 つの新しいオプションを含みます。

  • watchFile:個々のファイルがどのようにウォッチされるかの戦略。次の値を取ることができます。
    • fixedPollingInterval:一定間隔で一秒に数回、変更に対して全てのファイルをチェックします。
    • priorityPollingInterval:一秒に数回、変更に対して全てのファイルをチェックしますが、経験則を用いてファイルのある型を他よりもより少ない頻度でチェックします。
    • dynamicPriorityPolling:動的キューを用いて、より修正頻度の少ないファイルがより少ない回数でチェックされます。
    • useFsEvents:(デフォルト)ファイルの変更に対してオペレーティングシステム/ファイルシステムのネイティブイベントの利用を試みます。
    • useFsEventsOnParentDirectory:ディレクトリを含むディレクトリ上での変更をリッスンするため、オペレーティングシステム/ファイルシステムのネイティブイベントの利用を試みます。これはより少ないファイルウォッチャーを使用することができますが、より正確ではなくなったかもしれません。
  • watchDirectory:ディレクトリツリー全体が再帰的なファイルウォッチ機能に欠けたシステム化でどのようにウォッチされるかの戦略。次の値を取ることができます。
    • fixedPollingInterval:一定間隔で一秒に数回、変更に対して全てのディレクトリをチェックします。
    • dynamicPriorityPolling:動的キューを用いて、より修正頻度の少ないディレクトリがより少ない回数でチェックされます。
    • useFsEvents:(デフォルト)ディレクトリの変更に対してオペレーティングシステム/ファイルシステムのネイティブイベントの利用を試みます。
  • fallbackPolling:ファイルシステムイベントの利用時、システムがネイティブのファイルウォッチャーを持っていない、そして/あるいはサポートしていないときに使われる投票戦略をこのオプションは指定します。次の値を取ることができます。
    • fixedPollingInterval:(上述)
    • priorityPollingInterval:(上述)
    • dynamicPriorityPolling:(上述)
  • synchronousWatchDirectory:ディレクトリ上での遅延ウォッチを無効化します。遅延ウォッチは多くのファイルの変更が一度に発生するとき(例えばnpm installの実行時のnode_modules内での変更)に便利ですが、幾らかより一般的でない設定のために、このフラグによってそれを無効化したかったかもしれません。

これらの変更における更なる情報は、GitHub で pull requestを参照ください。

"Fast and Loose" Incremental Checking

TypeScript の--watchモードと--incrementalモードは、プロジェクトに対してフィードバックループを厳しくする手助けが可能です。--incrementalモードをオンにすることは、TypeScript にどのファイルが他に影響を与えうるかを追跡させることができ、それに加えて、--watchモードはコンパイラプロセスをオープンに保ち、可能な限りの量のメモリ内にある情報を再利用します。

しかしながら、もっと大きなプロジェクトに対しては、これらのオプションが我々に対して余裕のある速度での劇的な利益でさえ充分ではありません。例えば Visual Studio Code チームは、そのウォッチモード内で再チェック/再ビルドされる必要のあったファイルを査定する上ではより不正確だったであろう、gulp-tsbと呼ばれる TypeScript 周りでの彼ら独自のビルドツールを開発してきましたが、その結果、より極端に短いビルド時間を提供することができました。

ビルド速度のために正確さを犠牲にすることは、良くも悪くも、TypeScript/JavaScript の世界で多くの開発者が進んで作ろうとすることのトレードオフです。多くのユーザーは、エラーに対処することよりも彼らのイテレーションの時間を短くすることを前もって優先します。例として、型チェックや lint の結果にかかわらず、コードをビルドすることは極めて一般的です。

TypeScript 3.8 は新しいコンパイラオプションassumeChangesOnlyAffectDirectDependenciesを導入します。このオプションが有効化されたとき、TypeScript は本当に影響を受けた可能性のある全てのファイルを再チェック/再ビルドすることを避け、それらを直接インポートするファイルだけでなく、変更したファイルの再チェック/再ビルドだけを行うでしょう。

例えば、次のようにfileA.tsをインポートするfileB.tsを、インポートするfileC.tsを、インポートするfileD.tsファイルを考えてみてください。

fileA.ts <- fileB.ts <- fileC.ts <- fileD.ts

--watchモードでは、fileA.tsにおける変更は典型的には、TypeScript がfileB.ts, fileC.tsそしてfileD.tsの再チェックを少なくとも必要とすることを意味したでしょう。assumeChangesOnlyAffectDirectDependenciesの下では、fileA.tsにおける変更は、fileA.tsfileB.tsだけ再チェックされる必要があることを意味します。

Visual Studio Code のようなコードベースでは、これは特定のファイル内での変更に対する再ビルド時間をおよそ 14 秒からおよそ 1 秒に短縮します。全てのコードベースに対してこのオプションを必ずしもおすすめしませんが、極端に大きなコードベースをもち、後までずっとプロジェクト全体のエラーを先送りにしたい場合(例えば、tsconfig.fullbuild.json経由あるいは CI 内での特化したビルド)は、興味があったかもしれません。

更なる詳細は、元の pull requestをご確認ください。

Editor Features

Convert to Template String

Arooran Thanabalasingam (bigaru)のおかげで、TypeScript 3.8 は新しいリファクタリングを生み出し、次のような文字列の連結を

"I have " + numApples + " apples"

次のようなテンプレート文字列に変換します。

`I have ${numApples} apples`

convert-to-template-string

Call Hierarchy

与えられたファンクションの呼び出し元を把握するのはしばしば便利です。TypeScript は宣言の全ての参照を見つける方法をもっており(すなわちFind All Referencesコマンド)、ほとんどの人々はその質問に答えるためにそれを利用できます。しかし、それはわずかに厄介になり得ます。例えば、fooと名付けられたファクションの呼び出し元を探し出そうとすることを想像してください。

export function foo() {
    // ...
}

// later, much farther away from 'foo'...
export function bar() {
    foo();
}

export function baz() {
    foo()
}

foobarbazによって呼び出されたのだと発見すると、同様にbarbazも呼び出し元を知りたくなります!そのようにして、barbazに対しても同様にFind All Referencesを行使することができますが、元々答えようとしていた質問である「fooの呼び出し元は何か」のコンテキストを失います。

ここでのその制約に対処するため、幾らかのエディタはShow Call Hierarchyと呼ばれるコマンドを通じてファンクションが呼び出される経路を可視化する機能を持っており、TypeScript 3.8 は公式にCall Hierarchy機能をサポートします。

call-hierarchy-pic

Call Hierarchyは、最初は少しややこしかったかもしれませんし、その周辺の直感を築き上げるための利用法を必要とします。次のコードを考えてみましょう。

function frequentlyCalledFunction() {
    // do something useful
}

function callerA() {
    frequentlyCalledFunction();
}

function callerB() {
    callerA();
}

function callerC() {
    frequentlyCalledFunction();
}

function entryPoint() {
    callerA();
    callerB();
    callerC();
}

次のテキストのツリー図は、frequentlyCalledFunctionの call hierarchy を示します。

frequentlyCalledFunction

├─callerA
│ ├─ callerB
│ │ └─ entryPoint
│ │
│ └─ entryPoint

└─ callerC
└─ entryPoint

ここでは、frequentlyCalledFunctionの直近の呼び出し元がcallerAcallerBであることが見て取れます。callerAを呼び出すのは何かを知りたい場合、callerBと呼ばれるファンクションに沿って、プログラムのエントリーポイントがそれを直に呼び出すことを確認できます。callerBcallerCの呼び出し元をさらに展開して、それらがentryPointファンクション内でのみ呼ばれていることを確認できます。

Call HierarchyVisual Studio Code Insiders内の TypeScript/JavaScript に対して既にサポートされており、次の stable バージョンで利用可能になるでしょう。

call-hierarchy-gif

Breaking Changes

TypeScript 3.8 は、注記すべき少しのマイナーな破壊的変更を含んでいます。

Stricter Assignability Checks to Unions with Index Signatures

以前は、ユニオン型に割り当てられているときは excess プロパティはチェックされませんでした(たとえ、その excess プロパティがその index シのグネチャを一度も満たすことがなかったとしても)。TypeScript 3.8 では、型チェックがより厳しくなり、あるプロパティがインデックスのシグネチャをもっともらしく満たせる場合にのみ、そのプロパティは excess プロパティチェックを免除されます。

const obj1: { [x: string]: number } | { a: number };

obj1 = { a: 5, c: 'abc' }
//             ~
// Error!
// The type '{ [x: string]: number }' no longer exempts 'c'
// from excess property checks on '{ a: number }'.

let obj2: { [x: string]: number } | { [x: number]: number };

obj2 = { a: 'abc' };
//       ~
// Error!
// The types '{ [x: string]: number }' and '{ [x: number]: number }' no longer exempts 'a'
// from excess property checks against '{ [x: number]: number }',
// and it *is* sort of an excess property because 'a' isn't a numeric property name.
// This one is more subtle.

Optional Arguments with no Inferences are Correctly Marked as Implicitly any

次のコードでは、現在paramnoImplicitAnyのもとでエラーと共にマークされています。

function foo(f: () => void) {
    // ...
}

foo((param?) => {
    // ...
});

これは、foo中のfの型に対して一致するパラメータがないことが原因です。これは意図されたものではないように見えますが、paramに対して明示的な型を提供することで回避できます。

object in JSDoc is No Longer any Under noImplicitAny

歴史的には、JavaScript のチェックに対する TypeScript のサポートは、とっつきやすい経験を提供するために、特定の方法では緩いものでした。

例えば、"幾らかのオブジェクト、それが何かを知らない"を意味する、JSDoc 内のobjectをユーザーはしばしば利用しましたが、それはanyとして扱われてきました。

// @ts-check

/**
 * @param thing {Object} some object, i dunno what
 */
function doSomething(thing) {
    let x = thing.x;
    let y = thing.y;
    thing();
}

これは、それを TypeScript のObject型として扱うことがコード内では興味のないエラーを報告することになったことが原因であり、Object型がtoStringvalueOfのようなメソッド以外のわずかな能力と共に極端に曖昧な型である時からずっとです。

しかし、TypeScript はobject(ローワーケースのoであることに気づいてください)と名付けられたより便利な型を持っていますobject型は、Objectよりもさらに厳しく、その中ではstring, booleanそしてnumberのような全てのプリミティブ型を拒絶します。残念ながら、Objectobjectの両方は、JSDoc 中ではanyとして扱われていました。

objectが重宝され、JSDoc 中でObjectよりもかなり少ない頻度で使われていたために、noImplicitAnyを使った際は JavaScript ファイル内で特別扱いの振る舞いを削除してきましたが、その結果、JSDoc 内ではobject型はまさにそのノンプリミティブなobject型を参照します。

What's Next?

我々は次のバージョン TypeScript 3.9 が、2020 年の 5 月半ばに登場し、そのほとんどがパフォーマンス、洗練、そしてPromiseに対する本質的によりスマートな型チェックに焦点を当てるだろうと予測しています。来る日に我々の計画書は詳細のアイデアを提供するために公開されるでしょう。

しかし、3.9 までずっと待たないでください。つまり 3.8 は多くの素晴らしい戒厳に伴った非常に良いリリースですので、今日それを手に入れてください!

楽しんで、そして幸せなハッキングを!

- Daniel Rosenwasser and TypeScript Team

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Announcing TypeScript 3.8の和訳

Announcing TypeScript 3.8 の日本語訳です。


TypeScript 3.8 は多くの新機能をもたらし、中には新しいものや、まもなく公開される ECMAScript 標準機能、型に限定した import/export のための新しいシンタックス、そしてさらに多くのものがあります。

Index

Type-Only Imports and Exports

この機能は、ほとんどのユーザーは考える必要はないかもしれません。しかし、もしあなたがここでの問題に直面していたら、それは興味の対象かもしれません(特に --isolatedModulestranspileModule API、Babel の下でのコンパイル時)。

TypeScript は JavaScript の import シンタックスを再利用し、我々は型を参照することができます。例えば次の例では、JavaScript の値である doThing を、純粋な TypeScript の型である Options と共に import できます。

// ./foo.ts
interface Options {
    // ...
}

export function doThing(options: Options) {
    // ...
}

// ./bar.ts
import { doThing, Options } from "./foo.js";

function doThingBetter(options: Options) {
    // do something twice as good
    doThing(options);
    doThing(options);
}

これは便利です。なぜなら、import する時のほとんどは、何が import されたかを心配する必要がないからです。ただ、何かを import しているというだけです。

残念ながら、これは import elision と呼ばれる機能のために機能していただけでした。TypeScript が JavaScript ファイルを出力する時、Optionsが型としてのみ使用されるのを確認し、自動的にその import を削除します。その結果の出力は、このように見えるでしょう。

// ./foo.js
export function doThing(options: Options) {
    // ...
}

// ./bar.js
import { doThing } from "./foo.js";

function doThingBetter(options: Options) {
    // do something twice as good
    doThing(options);
    doThing(options);
}

改めて、この振る舞いは通常は素晴らしいですが、幾らか他の問題を引き起こします。

まず最初に、値や型が export されているかどうかが曖昧ないくつかの場所があります。例えば、次の例におけるMyThingは、値と型のどちらでしょうか。

import { MyThing } from "./some-module.js";

export { MyThing };

ちょうどこのファイルだけに限定すれば、知る由もありません。Babel と TypeSrcipt の transpileModule API の両方は、もし MyThing が型のみであるならば正しく機能しないコードを生成するでしょうし、TypeScript の isolatedModule フラグはそれが問題になることを警告するでしょう。本当の問題は、"いやいや、本当は私はちょうど型を指したんだよ、つまりこれは削除されるべきだ"と主張する方法がないことであり、そのために import elision はいまいちなのです。

他の問題は、TypeScript import elision が、型として使われた import だけが含まれた import 文を取り除くつもりであることでした。それは副作用を持つモジュール郡に対して目立った異なる振る舞いを引き起こし、そのためにユーザーは副作用を完璧に保証する二つ目の import 文を挿入しなければならなかったでしょう。

// This statement will get erased because of import elision.
import { SomeTypeFoo, SomeOtherTypeBar } from "./module-with-side-effects";

// This statement always sticks around.
import "./module-with-side-effects";

これが現れるのを見た具体的な場所は、Angular.js (1.x)のようなフレームワーク内であり、(副作用である)service がグローバルに登録される必要があり、しかしそれらの service は型に対してただ import されたものでした。

// ./service.ts
export class Service {
    // ...
}
register("globalServiceId", Service);

// ./consumer.ts
import { Service } from "./service.js";

inject("globalServiceId", function (service: Service) {
    // do stuff with Service
});

結果として、./service.jsは、一度も実行されることはなく、実行時に壊れるでしょう。

この類の問題を避けるために、種々のものがどのようにインポート/削除されていたかに関する、よりきめの細かい制御をユーザーに提供すべきだと気付きました。

TypeScript 3.8 における解決策として、型だけの import と export に対する新しいシンタックスを追加しました。

import type { SomeThing } from "./some-module.js";

export type { SomeThing };

import typeは、型アノテーションと宣言に対して使用される宣言だけをインポートします。それはいつも完全に削除された状態で、よって実行時にはそれの残存物はありません。同じように、export typeは型コンテキストに対して使用されうる export を提供するだけで、それもまた TypeScript の出力からは削除されます。

実行時に値を、設計時に型をクラスはもち、その利用はコンテキストの影響を受けることに注意してください。クラスをインポートするためにimport typeを使用する時、それから継承するようなことはできません。

import type { Component } from "react";

interface ButtonProps {
    // ...
}

class Button extends Component<ButtonProps> {
    //               ~~~~~~~~~
    // error! 'Component' only refers to a type, but is being used as a value here.

    // ...
}

もし以前のフローを使用してきたなら、そのシンタックスはかなり似ています。一つの違いは、不明瞭に見えたかもしれないコードを避けるために少しの制限を追加したことです。

// Is only 'Foo' a type? Or every declaration in the import?
// We just give an error because it's not clear.

import type Foo, { Bar, Baz } from "some-module";
//     ~~~~~~~~~~~~~~~~~~~~~~
// error! A type-only import can specify a default import or named bindings, but not both.

import typeと共に、実行時に利用されないであろう import に伴って発生することを制御するための、新しいコンパイラのフラグ importsNotUsedAsValues も追加しました。このフラグは 3 つの異なる値を取ります

  • remove:これはこれらの import を削除する現在の振る舞いです。デフォルトであり続けることになっており、破壊的変更ではありません。
  • preserve:これは一度も使われていない値を持つ全ての import を保存します。これは、import/副作用を保存させることができます。
  • error:これは全ての import を保存します(preserveオプションと全く同じです)が、値の import が型としてのみ使用されたときにエラーとなるでしょう。どの値も思わず import されていないことを保証したい時にこれは便利だったかもしれませんが、import に明示的な副作用を今もなおもたらします。

この機能についての更なる情報は、pull requestや、import type宣言からの import が使用されうる場所の周辺での関連した変更から確認できます。

ECMAScript Private Fields

TypeScript 3.8 は、stage-3 class fields proposalの一部である、ECMAScript の private フィールドに対するサポートをもたらします。この仕事は、Bloomberg での良き友達によって開始され、完成まで突き動かされました。

class Person {
    #name: string

    constructor(name: string) {
        this.#name = name;
    }

    greet() {
        console.log(`Hello, my name is ${this.#name}!`);
    }
}

let jeremy = new Person("Jeremy Bearimy");

jeremy.#name
//     ~~~~~
// Property '#name' is not accessible outside class 'Person'
// because it has a private identifier.

通常のプロパティとは(private修飾子と共に宣言されるものでさえ)違って、private フィールドは覚えておくべき少しのルールを持っています。それらのいくつかは:

  • private フィールドは#文字から始まります。時々、これらを private names と呼びます。
  • 全ての private フィールド名は、それを含むクラスの領域で一意です。
  • publicあるいはprivateのような TypeScript のアクセス修飾子は、private フィールド上では使用できません。
  • private フィールドは、それが含まれるクラスの外側では(JS ユーザーによってでさえも)アクセスあるいは発見さえできません。時々、これを hard privacy と呼びます。

"hard" privacy から離れて、private フィールドのもう一つの利点は、ちょうど言及した一意性です。例えば、通常のプロパティ宣言は、サブクラスにおいて上書きされる傾向があります。

class C {
    foo = 10;

    cHelper() {
        return this.foo;
    }
}

class D extends C {
    foo = 20;

    dHelper() {
        return this.foo;
    }
}

let instance = new D();
// 'this.foo' refers to the same property on each instance.
console.log(instance.cHelper()); // prints '20'
console.log(instance.dHelper()); // prints '20'

private フィールドに伴い、これについて心配する必要はなく、というのもそれぞれのフィールド名はそのクラスで一意であるためです。

class C {
    #foo = 10;

    cHelper() {
        return this.#foo;
    }
}

class D extends C {
    #foo = 20;

    dHelper() {
        return this.#foo;
    }
}

let instance = new D();
// 'this.#foo' refers to a different field within each class.
console.log(instance.cHelper()); // prints '10'
console.log(instance.dHelper()); // prints '20'

なきに等しいもう一つのことは、その他の型で private フィールドにアクセスすると、TypeError をもたらすであろうことです!

class Square {
    #sideLength: number;

    constructor(sideLength: number) {
        this.#sideLength = sideLength;
    }

    equals(other: any) {
        return this.#sideLength === other.#sideLength;
    }
}

const a = new Square(100);
const b = { sideLength: 100 };

// Boom!
// TypeError: attempted to get private field on non-instance
// This fails because 'b' is not an instance of 'Square'.
console.log(a.equals(b));

最後に、いかなる単純な.jsファイルユーザーに対し、private フィールドは常にそれらが割り当てられる前に宣言されなければなりません。

class C {
    // No declaration for '#foo'
    // :(

    constructor(foo: number) {
        // SyntaxError!
        // '#foo' needs to be declared before writing to it.
        this.#foo = foo;
    }
}

TypeScript はクラスのプロパティに対していつも宣言を要求してきたのに対して、JavaScript はいつも、宣言されていないプロパティにユーザーがアクセスすることを許してきました。private フィールドに伴い、.jsあるいは.tsファイルのどちらに取り組んでいるかに関係なく、宣言が常に要求されます。

class C {
    /** @type {number} */
    #foo;

    constructor(foo: number) {
        // This works.
        this.#foo = foo;
    }
}

この実装についての更なる情報は、元の pull requestから確認できます。

Which should I use?

TypeScript ユーザーとして private のどちらの種類を使用すべきかについてのたくさんの質問を受け取ってきました。つまり一般的には、"privateキーワードを使うべきか、あるいは ECMAScript の hash/pound (#) private フィールドを使うべきか"です。

全ての良い質問に対し、その回答は良いものではありません。それは場合によるのです!

プロパティに関しては、TypeScript の private 修飾子は常に削除されます(すなわち、実行時にそれは全体的に普通のプロパティのように振る舞い、そしてそれが private 修飾子によって宣言されたことを伝える手段はありません)。private キーワードを使用する時、privacy はコンパイル時/設計時においてのみ強制され、JavaScript コードを使用する者に対してそれは全体的に意図によるものです。

class C {
    private foo = 10;
}

// This is an error at compile time,
// but when TypeScript outputs .js files,
// it'll run fine and print '10'.
console.log(new C().foo);    // prints '10'
//                  ~~~
// error! Property 'foo' is private and only accessible within class 'C'.

// TypeScript allows this at compile-time
// as a "work-around" to avoid the error.
console.log(new C()["foo"]); // prints '10'

上側は、"soft privacy"のこの類が、幾らかの API に対するアクセスを持たないことをコードの使用者が一時的に回避する手助けをし、そしてまた、どんな実行時においても機能します。

一方で、ECMAScript の#private は、そのクラスの外側からは完全にアクセス不可能です。

class C {
    #foo = 10;
}

console.log(new C().#foo); // SyntaxError
//                  ~~~~
// TypeScript reports an error *and*
// this won't work at runtime!

console.log(new C()["#foo"]); // prints undefined
//          ~~~~~~~~~~~~~~~
// TypeScript reports an error under 'noImplicitAny',
// and this prints 'undefined'.

この hard privacy は、内部のいかなるものを誰も利用できないことを厳密に保証する上では本当に便利です。もしライブラリの作者なら、private フィールドの削除あるいは改名は、決して破壊的変更をもたらすべきではありません。

上述したように、もう一つの利点は、ECMAScript の#privates に伴ってサブクラス化が容易であることであり、なぜならそれらは本当にprivate であるからです。ECMAScript#private フィールドを使用する時、どのサブクラスも今までフィールド名の衝突を心配したことはありません。TypeScript の private プロパティ宣言に関しては、ユーザーは今もなおスーパークラスで宣言されたプロパティを再び宣言しないように気をつけなければなりません。

考慮すべきもう一つの事項は、コードをどこで実行するつもりなのかです。TypeScript は現在、ECMAScript 2015 (ES6)あるいはそれ以上の target を標的としない限り、この機能をサポートできません。これは、privacy を強制するために基準を下げた実装が WeakMap を使用しており、WeakMap はメモリリークを引き起こさない方法では polyfill され得ないからです。対照的に、TypeScript の private 宣言されたプロパティは、全ての target で(ECMAScript 3 でさえ)機能します。

最後の考慮は速さだったかもしれません。つまり、private プロパティは他のプロパティと違いがなく、そのためそれらへのアクセスは、どの runtime を target としていようとも、他のプロパティアクセスと同じくらい速いです。対照的に、#private フィールドは WeakMap を使って基準が下げられているため、使用するにはより遅いかもしれません。幾らかの runtime は#private フィールドの実際の実装を最適化し、速度の速い WeakMap の実装さえ持っていたかもしれない一方で、全ての runtime ではそうでなかったかもしれません。

export * as ns Syntax

単一のメンバーとしてもう一つのモジュールの全てのメンバーを公開するような、単一のエントリーポイントを持つことは、しばしば一般的です。

import * as utilities from "./utilities.js";
export { utilities };

これはとても一般的なので、ECMAScript 2020 は最近、このパターンをサポートする新しいシンタックスを追加しました。

export * as utilities from "./utilities.js";

これは JavaScript に対して良い QOL の改善であり、そして TypeScript 3.8 はこのシンタックスを実装しています。モジュールの target がes2020よりも以前であるとき、TypeScript は最初のコードスニペットに沿って何かしらを出力するでしょう。

この機能を実装したコミュニティメンバーであるWenlu Wang (Kingwl)氏には特に感謝いたします。更なる情報は、元の pull requestをご確認ください。

Top-Level await

JavaScript の中で(HTTP リクエストのような)I/O を提供するほとんどのモダンな環境は非同期であり、多くのモダンな API は Promise を返します。これはノンブロッキングな操作を作る上で多くの利点を持つ一方で、ファイルあるいは外部のコンテンツのロードのようなものを驚くほど面倒にします。

fetch("...")
    .then(response => response.text())
    .then(greeting => { console.log(greeting) });

Promise に伴う.thenチェインを避けるため、JavaScript ユーザーはawaitを使用するためにasyncfunction をしばしば導入し、そしてそれを定義した後でその function をすぐさま呼んでいました。

async function main() {
    const response = await fetch("...");
    const greeting = await response.text();
    console.log(greeting);
}

main()
    .catch(e => console.error(e))

asyncfunction の導入を避けるため、近く発表される ECMAScript の使いやすい機能、"top-level await"を使用することができます。

以前は JavaScript において(似た機能を伴う他のほとんどの言語に沿って)、awaitasyncfunction の本文中でのみ許可されていました。しかし、top-level await によって、awaitをモジュールのトップレベルで使用することができます。

const response = await fetch("...");
const greeting = await response.text();
console.log(greeting);

// Make sure we're a module
export {};

繊細さがあることの注記:top-level await はモジュールのトップレベルでのみ機能し、TypeScript がimportあるいはexportを見つけるとき、ファイルはモジュールとしてのみ見なされます。幾らかの基本的なケースにおいて、これを保証するためにexport {}を幾らかの定型として書き出す必要があったかもしれません。

top level await は、現時点であなたが期待する全ての環境では機能しないかもしれません。現在、target コンパイラオプションがes2017かそれ以上で、moduleexnextあるいはsystemであるときのみ、top level await を使用できます。いくつかの環境とバンドラーの中でのサポートは、制限されるかもしれず、あるいは実験的サポートを有効にすることを要求するかもしれません。

実装の更なる情報は、元の pull requestをご確認ください。

es2020 for target and module

Kagami Sascha Rosylight (saschanaz)のおかげで、TypeScript 3.8 はes2020moduletargetに対するオプションとしてサポートします。これは、optional chaining, nullish coalescing, export * as ns, そして動的import(...)シンタックスのようなより新しい ECMAScript 2020 の機能を保存するでしょう。それはまた、bigintリテラルが今やexnext配下で安定した target を持つことを意味します。

JSDoc Property Modifiers

TypeScript 3.8 はallowJsフラグをオンにすることで JavaScript ファイルをサポートし、またcheckJsオプションか// @ts-checkコメントを.jsファイルのトップに追加することによって、それらの JavaScript ファイルのtype-checkingをサポートします。

JavaScript ファイルは型チェックに対する熱心なシンタックスを持たないために、TypeScript は JSDoc を利用します。TypeScript 3.8 はプロパティに対する少しばかりの新しい JSDoc タグを理解します。

最初はアクセス修飾子です。つまり@public, @private, @protectedです。これらのタグは TypeScript 内でそれぞれ機能するpublic, private, protectedと全く同じように機能します。

// @ts-check

class Foo {
    constructor() {
        /** @private */
        this.stuff = 100;
    }

    printStuff() {
        console.log(this.stuff);
    }
}

new Foo().stuff;
//        ~~~~~
// error! Property 'stuff' is private and only accessible within class 'Foo'.
  • @publicはいつも暗に意味され、省略できますが、プロパティがどこからでも参照されることを意味します。
  • @privateは、プロパティがそれを含むクラス内でしか利用できないことを意味します。
  • @protectedは、プロパティがそれを含むクラスと全ての生成されたサブクラス内で利用でき、それを含むクラスの似ていないインスタンス上では利用できないことを意味します。

次に、プロパティが初期化中にのみ書かれることを保証するための修飾子@readonlyを追加しました。

// @ts-check

class Foo {
    constructor() {
        /** @readonly */
        this.stuff = 100;
    }

    writeToStuff() {
        this.stuff = 200;
        //   ~~~~~
        // Cannot assign to 'stuff' because it is a read-only property.
    }
}

new Foo().stuff++;
//        ~~~~~
// Cannot assign to 'stuff' because it is a read-only property.

Better Directory Watching on Linux and watchOptions

TypeScript 3.8 はディレクトリのウォッチに対する(node_modulesに対する変更を効率的にピックアップするのに重要な)新しい戦略を生み出します。

幾らかのコンテキストに対して、Linux のようなオペレーティングシステム上では、TypeScript は依存性における変更を発見するために、node_modules上に(ファイルウォッチャーとは対照的な)ディレクトリウォッチャーとそのサブディレクトリの多くをインストールします。これは、より少ないディレクトリを追跡するための方法が存在する一方で、利用可能なファイルウォッチャーの数がしばしばnode_modules内のファイルによって覆い隠されるためです。

TypeScript のより古いバージョンは、フォルダー上にディレクトリウォッチャーをすぐさまインストールしたでしょうし、起動時はうまくいっていたでしょう。しかし、npm インストールの間、多くの処理がnode_modules内で実行され、それは TypeScript を圧倒しうることもあり、しばしばエディターのセッションを鈍くします。これを防ぐため、これらのかなり不安定なディレクトリが安定するための幾らかの時間を提供するために、TypeScript 3.8 はディレクトリウォッチャーのインストールまでわずかに待ちます。

全てのプロジェクトが異なる戦略下でよりうまく機能したかもしれず、そしてこの新しいアプローチがあなたのワークフローに対して機能しないであろうことを理由に、ユーザーが戦略をウォッチしているコンパイラ/言語サービスがファイルとディレクトリの経過を追うために使用されるべきであることを伝えられる新しいwatchOptionsフィールドを、TypeScript 3.8 はtsconfig.jsonjsconfig.jsonに導入します。

{
    // Some typical compiler options
    "compilerOptions": {
        "target": "es2020",
        "moduleResolution": "node",
        // ...
    },

    // NEW: Options for file/directory watching
    "watchOptions": {
        // Use native file system events for files and directories
        "watchFile": "useFsEvents",
        "watchDirectory": "useFsEvents",

        // Poll files for updates more frequently
        // when they're updated a lot.
        "fallbackPolling": "dynamicPriority"
    }
}

watchOptionsは設定できる 4 つの新しいオプションを含みます。

  • watchFile:個々のファイルがどのようにウォッチされるかの戦略。次の値を取ることができます。
    • fixedPollingInterval:一定間隔で一秒に数回、変更に対して全てのファイルをチェックします。
    • priorityPollingInterval:一秒に数回、変更に対して全てのファイルをチェックしますが、経験則を用いてファイルのある型を他よりもより少ない頻度でチェックします。
    • dynamicPriorityPolling:動的キューを用いて、より修正頻度の少ないファイルがより少ない回数でチェックされます。
    • useFsEvents:(デフォルト)ファイルの変更に対してオペレーティングシステム/ファイルシステムのネイティブイベントの利用を試みます。
    • useFsEventsOnParentDirectory:ディレクトリを含むディレクトリ上での変更をリッスンするため、オペレーティングシステム/ファイルシステムのネイティブイベントの利用を試みます。これはより少ないファイルウォッチャーを使用することができますが、より正確ではなくなったかもしれません。
  • watchDirectory:ディレクトリツリー全体が再帰的なファイルウォッチ機能に欠けたシステム化でどのようにウォッチされるかの戦略。次の値を取ることができます。
    • fixedPollingInterval:一定間隔で一秒に数回、変更に対して全てのディレクトリをチェックします。
    • dynamicPriorityPolling:動的キューを用いて、より修正頻度の少ないディレクトリがより少ない回数でチェックされます。
    • useFsEvents:(デフォルト)ディレクトリの変更に対してオペレーティングシステム/ファイルシステムのネイティブイベントの利用を試みます。
  • fallbackPolling:ファイルシステムイベントの利用時、システムがネイティブのファイルウォッチャーを持っていない、そして/あるいはサポートしていないときに使われる投票戦略をこのオプションは指定します。次の値を取ることができます。
    • fixedPollingInterval:(上述)
    • priorityPollingInterval:(上述)
    • dynamicPriorityPolling:(上述)
  • synchronousWatchDirectory:ディレクトリ上での遅延ウォッチを無効化します。遅延ウォッチは多くのファイルの変更が一度に発生するとき(例えばnpm installの実行時のnode_modules内での変更)に便利ですが、幾らかより一般的でない設定のために、このフラグによってそれを無効化したかったかもしれません。

これらの変更における更なる情報は、GitHub で pull requestを参照ください。

"Fast and Loose" Incremental Checking

TypeScript の--watchモードと--incrementalモードは、プロジェクトに対してフィードバックループを厳しくする手助けが可能です。--incrementalモードをオンにすることは、TypeScript にどのファイルが他に影響を与えうるかを追跡させることができ、それに加えて、--watchモードはコンパイラプロセスをオープンに保ち、可能な限りの量のメモリ内にある情報を再利用します。

しかしながら、もっと大きなプロジェクトに対しては、これらのオプションが我々に対して余裕のある速度での劇的な利益でさえ充分ではありません。例えば Visual Studio Code チームは、そのウォッチモード内で再チェック/再ビルドされる必要のあったファイルを査定する上ではより不正確だったであろう、gulp-tsbと呼ばれる TypeScript 周りでの彼ら独自のビルドツールを開発してきましたが、その結果、より極端に短いビルド時間を提供することができました。

ビルド速度のために正確さを犠牲にすることは、良くも悪くも、TypeScript/JavaScript の世界で多くの開発者が進んで作ろうとすることのトレードオフです。多くのユーザーは、エラーに対処することよりも彼らのイテレーションの時間を短くすることを前もって優先します。例として、型チェックや lint の結果にかかわらず、コードをビルドすることは極めて一般的です。

TypeScript 3.8 は新しいコンパイラオプションassumeChangesOnlyAffectDirectDependenciesを導入します。このオプションが有効化されたとき、TypeScript は本当に影響を受けた可能性のある全てのファイルを再チェック/再ビルドすることを避け、それらを直接インポートするファイルだけでなく、変更したファイルの再チェック/再ビルドだけを行うでしょう。

例えば、次のようにfileA.tsをインポートするfileB.tsを、インポートするfileC.tsを、インポートするfileD.tsファイルを考えてみてください。

fileA.ts <- fileB.ts <- fileC.ts <- fileD.ts

--watchモードでは、fileA.tsにおける変更は典型的には、TypeScript がfileB.ts, fileC.tsそしてfileD.tsの再チェックを少なくとも必要とすることを意味したでしょう。assumeChangesOnlyAffectDirectDependenciesの下では、fileA.tsにおける変更は、fileA.tsfileB.tsだけ再チェックされる必要があることを意味します。

Visual Studio Code のようなコードベースでは、これは特定のファイル内での変更に対する再ビルド時間をおよそ 14 秒からおよそ 1 秒に短縮します。全てのコードベースに対してこのオプションを必ずしもおすすめしませんが、極端に大きなコードベースをもち、後までずっとプロジェクト全体のエラーを先送りにしたい場合(例えば、tsconfig.fullbuild.json経由あるいは CI 内での特化したビルド)は、興味があったかもしれません。

更なる詳細は、元の pull requestをご確認ください。

Editor Features

Convert to Template String

Arooran Thanabalasingam (bigaru)のおかげで、TypeScript 3.8 は新しいリファクタリングを生み出し、次のような文字列の連結を

"I have " + numApples + " apples"

次のようなテンプレート文字列に変換します。

`I have ${numApples} apples`

convert-to-template-string

Call Hierarchy

与えられたファンクションの呼び出し元を把握するのはしばしば便利です。TypeScript は宣言の全ての参照を見つける方法をもっており(すなわちFind All Referencesコマンド)、ほとんどの人々はその質問に答えるためにそれを利用できます。しかし、それはわずかに厄介になり得ます。例えば、fooと名付けられたファクションの呼び出し元を探し出そうとすることを想像してください。

export function foo() {
    // ...
}

// later, much farther away from 'foo'...
export function bar() {
    foo();
}

export function baz() {
    foo()
}

foobarbazによって呼び出されたのだと発見すると、同様にbarbazも呼び出し元を知りたくなります!そのようにして、barbazに対しても同様にFind All Referencesを行使することができますが、元々答えようとしていた質問である「fooの呼び出し元は何か」のコンテキストを失います。

ここでのその制約に対処するため、幾らかのエディタはShow Call Hierarchyと呼ばれるコマンドを通じてファンクションが呼び出される経路を可視化する機能を持っており、TypeScript 3.8 は公式にCall Hierarchy機能をサポートします。

call-hierarchy-pic

Call Hierarchyは、最初は少しややこしかったかもしれませんし、その周辺の直感を築き上げるための利用法を必要とします。次のコードを考えてみましょう。

function frequentlyCalledFunction() {
    // do something useful
}

function callerA() {
    frequentlyCalledFunction();
}

function callerB() {
    callerA();
}

function callerC() {
    frequentlyCalledFunction();
}

function entryPoint() {
    callerA();
    callerB();
    callerC();
}

次のテキストのツリー図は、frequentlyCalledFunctionの call hierarchy を示します。

frequentlyCalledFunction

├─callerA
│ ├─ callerB
│ │ └─ entryPoint
│ │
│ └─ entryPoint

└─ callerC
└─ entryPoint

ここでは、frequentlyCalledFunctionの直近の呼び出し元がcallerAcallerBであることが見て取れます。callerAを呼び出すのは何かを知りたい場合、callerBと呼ばれるファンクションに沿って、プログラムのエントリーポイントがそれを直に呼び出すことを確認できます。callerBcallerCの呼び出し元をさらに展開して、それらがentryPointファンクション内でのみ呼ばれていることを確認できます。

Call HierarchyVisual Studio Code Insiders内の TypeScript/JavaScript に対して既にサポートされており、次の stable バージョンで利用可能になるでしょう。

call-hierarchy-gif

Breaking Changes

TypeScript 3.8 は、注記すべき少しのマイナーな破壊的変更を含んでいます。

Stricter Assignability Checks to Unions with Index Signatures

以前は、ユニオン型に割り当てられているときは excess プロパティはチェックされませんでした(たとえ、その excess プロパティがその index シのグネチャを一度も満たすことがなかったとしても)。TypeScript 3.8 では、型チェックがより厳しくなり、あるプロパティがインデックスのシグネチャをもっともらしく満たせる場合にのみ、そのプロパティは excess プロパティチェックを免除されます。

const obj1: { [x: string]: number } | { a: number };

obj1 = { a: 5, c: 'abc' }
//             ~
// Error!
// The type '{ [x: string]: number }' no longer exempts 'c'
// from excess property checks on '{ a: number }'.

let obj2: { [x: string]: number } | { [x: number]: number };

obj2 = { a: 'abc' };
//       ~
// Error!
// The types '{ [x: string]: number }' and '{ [x: number]: number }' no longer exempts 'a'
// from excess property checks against '{ [x: number]: number }',
// and it *is* sort of an excess property because 'a' isn't a numeric property name.
// This one is more subtle.

Optional Arguments with no Inferences are Correctly Marked as Implicitly any

次のコードでは、現在paramnoImplicitAnyのもとでエラーと共にマークされています。

function foo(f: () => void) {
    // ...
}

foo((param?) => {
    // ...
});

これは、foo中のfの型に対して一致するパラメータがないことが原因です。これは意図されたものではないように見えますが、paramに対して明示的な型を提供することで回避できます。

object in JSDoc is No Longer any Under noImplicitAny

歴史的には、JavaScript のチェックに対する TypeScript のサポートは、とっつきやすい経験を提供するために、特定の方法では緩いものでした。

例えば、"幾らかのオブジェクト、それが何かを知らない"を意味する、JSDoc 内のobjectをユーザーはしばしば利用しましたが、それはanyとして扱われてきました。

// @ts-check

/**
 * @param thing {Object} some object, i dunno what
 */
function doSomething(thing) {
    let x = thing.x;
    let y = thing.y;
    thing();
}

これは、それを TypeScript のObject型として扱うことがコード内では興味のないエラーを報告することになったことが原因であり、Object型がtoStringvalueOfのようなメソッド以外のわずかな能力と共に極端に曖昧な型である時からずっとです。

しかし、TypeScript はobject(ローワーケースのoであることに気づいてください)と名付けられたより便利な型を持っていますobject型は、Objectよりもさらに厳しく、その中ではstring, booleanそしてnumberのような全てのプリミティブ型を拒絶します。残念ながら、Objectobjectの両方は、JSDoc 中ではanyとして扱われていました。

objectが重宝され、JSDoc 中でObjectよりもかなり少ない頻度で使われていたために、noImplicitAnyを使った際は JavaScript ファイル内で特別扱いの振る舞いを削除してきましたが、その結果、JSDoc 内ではobject型はまさにそのノンプリミティブなobject型を参照します。

What's Next?

我々は次のバージョン TypeScript 3.9 が、2020 年の 5 月半ばに登場し、そのほとんどがパフォーマンス、洗練、そしてPromiseに対する本質的によりスマートな型チェックに焦点を当てるだろうと予測しています。来る日に我々の計画書は詳細のアイデアを提供するために公開されるでしょう。

しかし、3.9 までずっと待たないでください。つまり 3.8 は多くの素晴らしい戒厳に伴った非常に良いリリースですので、今日それを手に入れてください!

楽しんで、そして幸せなハッキングを!

- Daniel Rosenwasser and TypeScript Team

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[和訳]Announcing TypeScript 3.8

Announcing TypeScript 3.8 の日本語訳です。


TypeScript 3.8 は多くの新機能をもたらし、中には新しいものや、まもなく公開される ECMAScript 標準機能、型に限定した import/export のための新しいシンタックス、そしてさらに多くのものがあります。

Index

Type-Only Imports and Exports

この機能は、ほとんどのユーザーは考える必要はないかもしれません。しかし、もしあなたがここでの問題に直面していたら、それは興味の対象かもしれません(特に --isolatedModulestranspileModule API、Babel の下でのコンパイル時)。

TypeScript は JavaScript の import シンタックスを再利用し、我々は型を参照することができます。例えば次の例では、JavaScript の値である doThing を、純粋な TypeScript の型である Options と共に import できます。

// ./foo.ts
interface Options {
    // ...
}

export function doThing(options: Options) {
    // ...
}

// ./bar.ts
import { doThing, Options } from "./foo.js";

function doThingBetter(options: Options) {
    // do something twice as good
    doThing(options);
    doThing(options);
}

これは便利です。なぜなら、import する時のほとんどは、何が import されたかを心配する必要がないからです。ただ、何かを import しているというだけです。

残念ながら、これは import elision と呼ばれる機能のために機能していただけでした。TypeScript が JavaScript ファイルを出力する時、Optionsが型としてのみ使用されるのを確認し、自動的にその import を削除します。その結果の出力は、このように見えるでしょう。

// ./foo.js
export function doThing(options: Options) {
    // ...
}

// ./bar.js
import { doThing } from "./foo.js";

function doThingBetter(options: Options) {
    // do something twice as good
    doThing(options);
    doThing(options);
}

改めて、この振る舞いは通常は素晴らしいですが、幾らか他の問題を引き起こします。

まず最初に、値や型が export されているかどうかが曖昧ないくつかの場所があります。例えば、次の例におけるMyThingは、値と型のどちらでしょうか。

import { MyThing } from "./some-module.js";

export { MyThing };

ちょうどこのファイルだけに限定すれば、知る由もありません。Babel と TypeSrcipt の transpileModule API の両方は、もし MyThing が型のみであるならば正しく機能しないコードを生成するでしょうし、TypeScript の isolatedModule フラグはそれが問題になることを警告するでしょう。本当の問題は、"いやいや、本当は私はちょうど型を指したんだよ、つまりこれは削除されるべきだ"と主張する方法がないことであり、そのために import elision はいまいちなのです。

他の問題は、TypeScript import elision が、型として使われた import だけが含まれた import 文を取り除くつもりであることでした。それは副作用を持つモジュール郡に対して目立った異なる振る舞いを引き起こし、そのためにユーザーは副作用を完璧に保証する二つ目の import 文を挿入しなければならなかったでしょう。

// This statement will get erased because of import elision.
import { SomeTypeFoo, SomeOtherTypeBar } from "./module-with-side-effects";

// This statement always sticks around.
import "./module-with-side-effects";

これが現れるのを見た具体的な場所は、Angular.js (1.x)のようなフレームワーク内であり、(副作用である)service がグローバルに登録される必要があり、しかしそれらの service は型に対してただ import されたものでした。

// ./service.ts
export class Service {
    // ...
}
register("globalServiceId", Service);

// ./consumer.ts
import { Service } from "./service.js";

inject("globalServiceId", function (service: Service) {
    // do stuff with Service
});

結果として、./service.jsは、一度も実行されることはなく、実行時に壊れるでしょう。

この類の問題を避けるために、種々のものがどのようにインポート/削除されていたかに関する、よりきめの細かい制御をユーザーに提供すべきだと気付きました。

TypeScript 3.8 における解決策として、型だけの import と export に対する新しいシンタックスを追加しました。

import type { SomeThing } from "./some-module.js";

export type { SomeThing };

import typeは、型アノテーションと宣言に対して使用される宣言だけをインポートします。それはいつも完全に削除された状態で、よって実行時にはそれの残存物はありません。同じように、export typeは型コンテキストに対して使用されうる export を提供するだけで、それもまた TypeScript の出力からは削除されます。

実行時に値を、設計時に型をクラスはもち、その利用はコンテキストの影響を受けることに注意してください。クラスをインポートするためにimport typeを使用する時、それから継承するようなことはできません。

import type { Component } from "react";

interface ButtonProps {
    // ...
}

class Button extends Component<ButtonProps> {
    //               ~~~~~~~~~
    // error! 'Component' only refers to a type, but is being used as a value here.

    // ...
}

もし以前のフローを使用してきたなら、そのシンタックスはかなり似ています。一つの違いは、不明瞭に見えたかもしれないコードを避けるために少しの制限を追加したことです。

// Is only 'Foo' a type? Or every declaration in the import?
// We just give an error because it's not clear.

import type Foo, { Bar, Baz } from "some-module";
//     ~~~~~~~~~~~~~~~~~~~~~~
// error! A type-only import can specify a default import or named bindings, but not both.

import typeと共に、実行時に利用されないであろう import に伴って発生することを制御するための、新しいコンパイラのフラグ importsNotUsedAsValues も追加しました。このフラグは 3 つの異なる値を取ります

  • remove:これはこれらの import を削除する現在の振る舞いです。デフォルトであり続けることになっており、破壊的変更ではありません。
  • preserve:これは一度も使われていない値を持つ全ての import を保存します。これは、import/副作用を保存させることができます。
  • error:これは全ての import を保存します(preserveオプションと全く同じです)が、値の import が型としてのみ使用されたときにエラーとなるでしょう。どの値も思わず import されていないことを保証したい時にこれは便利だったかもしれませんが、import に明示的な副作用を今もなおもたらします。

この機能についての更なる情報は、pull requestや、import type宣言からの import が使用されうる場所の周辺での関連した変更から確認できます。

ECMAScript Private Fields

TypeScript 3.8 は、stage-3 class fields proposalの一部である、ECMAScript の private フィールドに対するサポートをもたらします。この仕事は、Bloomberg での良き友達によって開始され、完成まで突き動かされました。

class Person {
    #name: string

    constructor(name: string) {
        this.#name = name;
    }

    greet() {
        console.log(`Hello, my name is ${this.#name}!`);
    }
}

let jeremy = new Person("Jeremy Bearimy");

jeremy.#name
//     ~~~~~
// Property '#name' is not accessible outside class 'Person'
// because it has a private identifier.

通常のプロパティとは(private修飾子と共に宣言されるものでさえ)違って、private フィールドは覚えておくべき少しのルールを持っています。それらのいくつかは:

  • private フィールドは#文字から始まります。時々、これらを private names と呼びます。
  • 全ての private フィールド名は、それを含むクラスの領域で一意です。
  • publicあるいはprivateのような TypeScript のアクセス修飾子は、private フィールド上では使用できません。
  • private フィールドは、それが含まれるクラスの外側では(JS ユーザーによってでさえも)アクセスあるいは発見さえできません。時々、これを hard privacy と呼びます。

"hard" privacy から離れて、private フィールドのもう一つの利点は、ちょうど言及した一意性です。例えば、通常のプロパティ宣言は、サブクラスにおいて上書きされる傾向があります。

class C {
    foo = 10;

    cHelper() {
        return this.foo;
    }
}

class D extends C {
    foo = 20;

    dHelper() {
        return this.foo;
    }
}

let instance = new D();
// 'this.foo' refers to the same property on each instance.
console.log(instance.cHelper()); // prints '20'
console.log(instance.dHelper()); // prints '20'

private フィールドに伴い、これについて心配する必要はなく、というのもそれぞれのフィールド名はそのクラスで一意であるためです。

class C {
    #foo = 10;

    cHelper() {
        return this.#foo;
    }
}

class D extends C {
    #foo = 20;

    dHelper() {
        return this.#foo;
    }
}

let instance = new D();
// 'this.#foo' refers to a different field within each class.
console.log(instance.cHelper()); // prints '10'
console.log(instance.dHelper()); // prints '20'

なきに等しいもう一つのことは、その他の型で private フィールドにアクセスすると、TypeError をもたらすであろうことです!

class Square {
    #sideLength: number;

    constructor(sideLength: number) {
        this.#sideLength = sideLength;
    }

    equals(other: any) {
        return this.#sideLength === other.#sideLength;
    }
}

const a = new Square(100);
const b = { sideLength: 100 };

// Boom!
// TypeError: attempted to get private field on non-instance
// This fails because 'b' is not an instance of 'Square'.
console.log(a.equals(b));

最後に、いかなる単純な.jsファイルユーザーに対し、private フィールドは常にそれらが割り当てられる前に宣言されなければなりません。

class C {
    // No declaration for '#foo'
    // :(

    constructor(foo: number) {
        // SyntaxError!
        // '#foo' needs to be declared before writing to it.
        this.#foo = foo;
    }
}

TypeScript はクラスのプロパティに対していつも宣言を要求してきたのに対して、JavaScript はいつも、宣言されていないプロパティにユーザーがアクセスすることを許してきました。private フィールドに伴い、.jsあるいは.tsファイルのどちらに取り組んでいるかに関係なく、宣言が常に要求されます。

class C {
    /** @type {number} */
    #foo;

    constructor(foo: number) {
        // This works.
        this.#foo = foo;
    }
}

この実装についての更なる情報は、元の pull requestから確認できます。

Which should I use?

TypeScript ユーザーとして private のどちらの種類を使用すべきかについてのたくさんの質問を受け取ってきました。つまり一般的には、"privateキーワードを使うべきか、あるいは ECMAScript の hash/pound (#) private フィールドを使うべきか"です。

全ての良い質問に対し、その回答は良いものではありません。それは場合によるのです!

プロパティに関しては、TypeScript の private 修飾子は常に削除されます(すなわち、実行時にそれは全体的に普通のプロパティのように振る舞い、そしてそれが private 修飾子によって宣言されたことを伝える手段はありません)。private キーワードを使用する時、privacy はコンパイル時/設計時においてのみ強制され、JavaScript コードを使用する者に対してそれは全体的に意図によるものです。

class C {
    private foo = 10;
}

// This is an error at compile time,
// but when TypeScript outputs .js files,
// it'll run fine and print '10'.
console.log(new C().foo);    // prints '10'
//                  ~~~
// error! Property 'foo' is private and only accessible within class 'C'.

// TypeScript allows this at compile-time
// as a "work-around" to avoid the error.
console.log(new C()["foo"]); // prints '10'

上側は、"soft privacy"のこの類が、幾らかの API に対するアクセスを持たないことをコードの使用者が一時的に回避する手助けをし、そしてまた、どんな実行時においても機能します。

一方で、ECMAScript の#private は、そのクラスの外側からは完全にアクセス不可能です。

class C {
    #foo = 10;
}

console.log(new C().#foo); // SyntaxError
//                  ~~~~
// TypeScript reports an error *and*
// this won't work at runtime!

console.log(new C()["#foo"]); // prints undefined
//          ~~~~~~~~~~~~~~~
// TypeScript reports an error under 'noImplicitAny',
// and this prints 'undefined'.

この hard privacy は、内部のいかなるものを誰も利用できないことを厳密に保証する上では本当に便利です。もしライブラリの作者なら、private フィールドの削除あるいは改名は、決して破壊的変更をもたらすべきではありません。

上述したように、もう一つの利点は、ECMAScript の#privates に伴ってサブクラス化が容易であることであり、なぜならそれらは本当にprivate であるからです。ECMAScript#private フィールドを使用する時、どのサブクラスも今までフィールド名の衝突を心配したことはありません。TypeScript の private プロパティ宣言に関しては、ユーザーは今もなおスーパークラスで宣言されたプロパティを再び宣言しないように気をつけなければなりません。

考慮すべきもう一つの事項は、コードをどこで実行するつもりなのかです。TypeScript は現在、ECMAScript 2015 (ES6)あるいはそれ以上の target を標的としない限り、この機能をサポートできません。これは、privacy を強制するために基準を下げた実装が WeakMap を使用しており、WeakMap はメモリリークを引き起こさない方法では polyfill され得ないからです。対照的に、TypeScript の private 宣言されたプロパティは、全ての target で(ECMAScript 3 でさえ)機能します。

最後の考慮は速さだったかもしれません。つまり、private プロパティは他のプロパティと違いがなく、そのためそれらへのアクセスは、どの runtime を target としていようとも、他のプロパティアクセスと同じくらい速いです。対照的に、#private フィールドは WeakMap を使って基準が下げられているため、使用するにはより遅いかもしれません。幾らかの runtime は#private フィールドの実際の実装を最適化し、速度の速い WeakMap の実装さえ持っていたかもしれない一方で、全ての runtime ではそうでなかったかもしれません。

export * as ns Syntax

単一のメンバーとしてもう一つのモジュールの全てのメンバーを公開するような、単一のエントリーポイントを持つことは、しばしば一般的です。

import * as utilities from "./utilities.js";
export { utilities };

これはとても一般的なので、ECMAScript 2020 は最近、このパターンをサポートする新しいシンタックスを追加しました。

export * as utilities from "./utilities.js";

これは JavaScript に対して良い QOL の改善であり、そして TypeScript 3.8 はこのシンタックスを実装しています。モジュールの target がes2020よりも以前であるとき、TypeScript は最初のコードスニペットに沿って何かしらを出力するでしょう。

この機能を実装したコミュニティメンバーであるWenlu Wang (Kingwl)氏には特に感謝いたします。更なる情報は、元の pull requestをご確認ください。

Top-Level await

JavaScript の中で(HTTP リクエストのような)I/O を提供するほとんどのモダンな環境は非同期であり、多くのモダンな API は Promise を返します。これはノンブロッキングな操作を作る上で多くの利点を持つ一方で、ファイルあるいは外部のコンテンツのロードのようなものを驚くほど面倒にします。

fetch("...")
    .then(response => response.text())
    .then(greeting => { console.log(greeting) });

Promise に伴う.thenチェインを避けるため、JavaScript ユーザーはawaitを使用するためにasyncfunction をしばしば導入し、そしてそれを定義した後でその function をすぐさま呼んでいました。

async function main() {
    const response = await fetch("...");
    const greeting = await response.text();
    console.log(greeting);
}

main()
    .catch(e => console.error(e))

asyncfunction の導入を避けるため、近く発表される ECMAScript の使いやすい機能、"top-level await"を使用することができます。

以前は JavaScript において(似た機能を伴う他のほとんどの言語に沿って)、awaitasyncfunction の本文中でのみ許可されていました。しかし、top-level await によって、awaitをモジュールのトップレベルで使用することができます。

const response = await fetch("...");
const greeting = await response.text();
console.log(greeting);

// Make sure we're a module
export {};

繊細さがあることの注記:top-level await はモジュールのトップレベルでのみ機能し、TypeScript がimportあるいはexportを見つけるとき、ファイルはモジュールとしてのみ見なされます。幾らかの基本的なケースにおいて、これを保証するためにexport {}を幾らかの定型として書き出す必要があったかもしれません。

top level await は、現時点であなたが期待する全ての環境では機能しないかもしれません。現在、target コンパイラオプションがes2017かそれ以上で、moduleexnextあるいはsystemであるときのみ、top level await を使用できます。いくつかの環境とバンドラーの中でのサポートは、制限されるかもしれず、あるいは実験的サポートを有効にすることを要求するかもしれません。

実装の更なる情報は、元の pull requestをご確認ください。

es2020 for target and module

Kagami Sascha Rosylight (saschanaz)のおかげで、TypeScript 3.8 はes2020moduletargetに対するオプションとしてサポートします。これは、optional chaining, nullish coalescing, export * as ns, そして動的import(...)シンタックスのようなより新しい ECMAScript 2020 の機能を保存するでしょう。それはまた、bigintリテラルが今やexnext配下で安定した target を持つことを意味します。

JSDoc Property Modifiers

TypeScript 3.8 はallowJsフラグをオンにすることで JavaScript ファイルをサポートし、またcheckJsオプションか// @ts-checkコメントを.jsファイルのトップに追加することによって、それらの JavaScript ファイルのtype-checkingをサポートします。

JavaScript ファイルは型チェックに対する熱心なシンタックスを持たないために、TypeScript は JSDoc を利用します。TypeScript 3.8 はプロパティに対する少しばかりの新しい JSDoc タグを理解します。

最初はアクセス修飾子です。つまり@public, @private, @protectedです。これらのタグは TypeScript 内でそれぞれ機能するpublic, private, protectedと全く同じように機能します。

// @ts-check

class Foo {
    constructor() {
        /** @private */
        this.stuff = 100;
    }

    printStuff() {
        console.log(this.stuff);
    }
}

new Foo().stuff;
//        ~~~~~
// error! Property 'stuff' is private and only accessible within class 'Foo'.
  • @publicはいつも暗に意味され、省略できますが、プロパティがどこからでも参照されることを意味します。
  • @privateは、プロパティがそれを含むクラス内でしか利用できないことを意味します。
  • @protectedは、プロパティがそれを含むクラスと全ての生成されたサブクラス内で利用でき、それを含むクラスの似ていないインスタンス上では利用できないことを意味します。

次に、プロパティが初期化中にのみ書かれることを保証するための修飾子@readonlyを追加しました。

// @ts-check

class Foo {
    constructor() {
        /** @readonly */
        this.stuff = 100;
    }

    writeToStuff() {
        this.stuff = 200;
        //   ~~~~~
        // Cannot assign to 'stuff' because it is a read-only property.
    }
}

new Foo().stuff++;
//        ~~~~~
// Cannot assign to 'stuff' because it is a read-only property.

Better Directory Watching on Linux and watchOptions

TypeScript 3.8 はディレクトリのウォッチに対する(node_modulesに対する変更を効率的にピックアップするのに重要な)新しい戦略を生み出します。

幾らかのコンテキストに対して、Linux のようなオペレーティングシステム上では、TypeScript は依存性における変更を発見するために、node_modules上に(ファイルウォッチャーとは対照的な)ディレクトリウォッチャーとそのサブディレクトリの多くをインストールします。これは、より少ないディレクトリを追跡するための方法が存在する一方で、利用可能なファイルウォッチャーの数がしばしばnode_modules内のファイルによって覆い隠されるためです。

TypeScript のより古いバージョンは、フォルダー上にディレクトリウォッチャーをすぐさまインストールしたでしょうし、起動時はうまくいっていたでしょう。しかし、npm インストールの間、多くの処理がnode_modules内で実行され、それは TypeScript を圧倒しうることもあり、しばしばエディターのセッションを鈍くします。これを防ぐため、これらのかなり不安定なディレクトリが安定するための幾らかの時間を提供するために、TypeScript 3.8 はディレクトリウォッチャーのインストールまでわずかに待ちます。

全てのプロジェクトが異なる戦略下でよりうまく機能したかもしれず、そしてこの新しいアプローチがあなたのワークフローに対して機能しないであろうことを理由に、ユーザーが戦略をウォッチしているコンパイラ/言語サービスがファイルとディレクトリの経過を追うために使用されるべきであることを伝えられる新しいwatchOptionsフィールドを、TypeScript 3.8 はtsconfig.jsonjsconfig.jsonに導入します。

{
    // Some typical compiler options
    "compilerOptions": {
        "target": "es2020",
        "moduleResolution": "node",
        // ...
    },

    // NEW: Options for file/directory watching
    "watchOptions": {
        // Use native file system events for files and directories
        "watchFile": "useFsEvents",
        "watchDirectory": "useFsEvents",

        // Poll files for updates more frequently
        // when they're updated a lot.
        "fallbackPolling": "dynamicPriority"
    }
}

watchOptionsは設定できる 4 つの新しいオプションを含みます。

  • watchFile:個々のファイルがどのようにウォッチされるかの戦略。次の値を取ることができます。
    • fixedPollingInterval:一定間隔で一秒に数回、変更に対して全てのファイルをチェックします。
    • priorityPollingInterval:一秒に数回、変更に対して全てのファイルをチェックしますが、経験則を用いてファイルのある型を他よりもより少ない頻度でチェックします。
    • dynamicPriorityPolling:動的キューを用いて、より修正頻度の少ないファイルがより少ない回数でチェックされます。
    • useFsEvents:(デフォルト)ファイルの変更に対してオペレーティングシステム/ファイルシステムのネイティブイベントの利用を試みます。
    • useFsEventsOnParentDirectory:ディレクトリを含むディレクトリ上での変更をリッスンするため、オペレーティングシステム/ファイルシステムのネイティブイベントの利用を試みます。これはより少ないファイルウォッチャーを使用することができますが、より正確ではなくなったかもしれません。
  • watchDirectory:ディレクトリツリー全体が再帰的なファイルウォッチ機能に欠けたシステム化でどのようにウォッチされるかの戦略。次の値を取ることができます。
    • fixedPollingInterval:一定間隔で一秒に数回、変更に対して全てのディレクトリをチェックします。
    • dynamicPriorityPolling:動的キューを用いて、より修正頻度の少ないディレクトリがより少ない回数でチェックされます。
    • useFsEvents:(デフォルト)ディレクトリの変更に対してオペレーティングシステム/ファイルシステムのネイティブイベントの利用を試みます。
  • fallbackPolling:ファイルシステムイベントの利用時、システムがネイティブのファイルウォッチャーを持っていない、そして/あるいはサポートしていないときに使われる投票戦略をこのオプションは指定します。次の値を取ることができます。
    • fixedPollingInterval:(上述)
    • priorityPollingInterval:(上述)
    • dynamicPriorityPolling:(上述)
  • synchronousWatchDirectory:ディレクトリ上での遅延ウォッチを無効化します。遅延ウォッチは多くのファイルの変更が一度に発生するとき(例えばnpm installの実行時のnode_modules内での変更)に便利ですが、幾らかより一般的でない設定のために、このフラグによってそれを無効化したかったかもしれません。

これらの変更における更なる情報は、GitHub で pull requestを参照ください。

"Fast and Loose" Incremental Checking

TypeScript の--watchモードと--incrementalモードは、プロジェクトに対してフィードバックループを厳しくする手助けが可能です。--incrementalモードをオンにすることは、TypeScript にどのファイルが他に影響を与えうるかを追跡させることができ、それに加えて、--watchモードはコンパイラプロセスをオープンに保ち、可能な限りの量のメモリ内にある情報を再利用します。

しかしながら、もっと大きなプロジェクトに対しては、これらのオプションが我々に対して余裕のある速度での劇的な利益でさえ充分ではありません。例えば Visual Studio Code チームは、そのウォッチモード内で再チェック/再ビルドされる必要のあったファイルを査定する上ではより不正確だったであろう、gulp-tsbと呼ばれる TypeScript 周りでの彼ら独自のビルドツールを開発してきましたが、その結果、より極端に短いビルド時間を提供することができました。

ビルド速度のために正確さを犠牲にすることは、良くも悪くも、TypeScript/JavaScript の世界で多くの開発者が進んで作ろうとすることのトレードオフです。多くのユーザーは、エラーに対処することよりも彼らのイテレーションの時間を短くすることを前もって優先します。例として、型チェックや lint の結果にかかわらず、コードをビルドすることは極めて一般的です。

TypeScript 3.8 は新しいコンパイラオプションassumeChangesOnlyAffectDirectDependenciesを導入します。このオプションが有効化されたとき、TypeScript は本当に影響を受けた可能性のある全てのファイルを再チェック/再ビルドすることを避け、それらを直接インポートするファイルだけでなく、変更したファイルの再チェック/再ビルドだけを行うでしょう。

例えば、次のようにfileA.tsをインポートするfileB.tsを、インポートするfileC.tsを、インポートするfileD.tsファイルを考えてみてください。

fileA.ts <- fileB.ts <- fileC.ts <- fileD.ts

--watchモードでは、fileA.tsにおける変更は典型的には、TypeScript がfileB.ts, fileC.tsそしてfileD.tsの再チェックを少なくとも必要とすることを意味したでしょう。assumeChangesOnlyAffectDirectDependenciesの下では、fileA.tsにおける変更は、fileA.tsfileB.tsだけ再チェックされる必要があることを意味します。

Visual Studio Code のようなコードベースでは、これは特定のファイル内での変更に対する再ビルド時間をおよそ 14 秒からおよそ 1 秒に短縮します。全てのコードベースに対してこのオプションを必ずしもおすすめしませんが、極端に大きなコードベースをもち、後までずっとプロジェクト全体のエラーを先送りにしたい場合(例えば、tsconfig.fullbuild.json経由あるいは CI 内での特化したビルド)は、興味があったかもしれません。

更なる詳細は、元の pull requestをご確認ください。

Editor Features

Convert to Template String

Arooran Thanabalasingam (bigaru)のおかげで、TypeScript 3.8 は新しいリファクタリングを生み出し、次のような文字列の連結を

"I have " + numApples + " apples"

次のようなテンプレート文字列に変換します。

`I have ${numApples} apples`

convert-to-template-string

Call Hierarchy

与えられたファンクションの呼び出し元を把握するのはしばしば便利です。TypeScript は宣言の全ての参照を見つける方法をもっており(すなわちFind All Referencesコマンド)、ほとんどの人々はその質問に答えるためにそれを利用できます。しかし、それはわずかに厄介になり得ます。例えば、fooと名付けられたファクションの呼び出し元を探し出そうとすることを想像してください。

export function foo() {
    // ...
}

// later, much farther away from 'foo'...
export function bar() {
    foo();
}

export function baz() {
    foo()
}

foobarbazによって呼び出されたのだと発見すると、同様にbarbazも呼び出し元を知りたくなります!そのようにして、barbazに対しても同様にFind All Referencesを行使することができますが、元々答えようとしていた質問である「fooの呼び出し元は何か」のコンテキストを失います。

ここでのその制約に対処するため、幾らかのエディタはShow Call Hierarchyと呼ばれるコマンドを通じてファンクションが呼び出される経路を可視化する機能を持っており、TypeScript 3.8 は公式にCall Hierarchy機能をサポートします。

call-hierarchy-pic

Call Hierarchyは、最初は少しややこしかったかもしれませんし、その周辺の直感を築き上げるための利用法を必要とします。次のコードを考えてみましょう。

function frequentlyCalledFunction() {
    // do something useful
}

function callerA() {
    frequentlyCalledFunction();
}

function callerB() {
    callerA();
}

function callerC() {
    frequentlyCalledFunction();
}

function entryPoint() {
    callerA();
    callerB();
    callerC();
}

次のテキストのツリー図は、frequentlyCalledFunctionの call hierarchy を示します。

frequentlyCalledFunction

├─callerA
│ ├─ callerB
│ │ └─ entryPoint
│ │
│ └─ entryPoint

└─ callerC
└─ entryPoint

ここでは、frequentlyCalledFunctionの直近の呼び出し元がcallerAcallerBであることが見て取れます。callerAを呼び出すのは何かを知りたい場合、callerBと呼ばれるファンクションに沿って、プログラムのエントリーポイントがそれを直に呼び出すことを確認できます。callerBcallerCの呼び出し元をさらに展開して、それらがentryPointファンクション内でのみ呼ばれていることを確認できます。

Call HierarchyVisual Studio Code Insiders内の TypeScript/JavaScript に対して既にサポートされており、次の stable バージョンで利用可能になるでしょう。

call-hierarchy-gif

Breaking Changes

TypeScript 3.8 は、注記すべき少しのマイナーな破壊的変更を含んでいます。

Stricter Assignability Checks to Unions with Index Signatures

以前は、ユニオン型に割り当てられているときは excess プロパティはチェックされませんでした(たとえ、その excess プロパティがその index シのグネチャを一度も満たすことがなかったとしても)。TypeScript 3.8 では、型チェックがより厳しくなり、あるプロパティがインデックスのシグネチャをもっともらしく満たせる場合にのみ、そのプロパティは excess プロパティチェックを免除されます。

const obj1: { [x: string]: number } | { a: number };

obj1 = { a: 5, c: 'abc' }
//             ~
// Error!
// The type '{ [x: string]: number }' no longer exempts 'c'
// from excess property checks on '{ a: number }'.

let obj2: { [x: string]: number } | { [x: number]: number };

obj2 = { a: 'abc' };
//       ~
// Error!
// The types '{ [x: string]: number }' and '{ [x: number]: number }' no longer exempts 'a'
// from excess property checks against '{ [x: number]: number }',
// and it *is* sort of an excess property because 'a' isn't a numeric property name.
// This one is more subtle.

Optional Arguments with no Inferences are Correctly Marked as Implicitly any

次のコードでは、現在paramnoImplicitAnyのもとでエラーと共にマークされています。

function foo(f: () => void) {
    // ...
}

foo((param?) => {
    // ...
});

これは、foo中のfの型に対して一致するパラメータがないことが原因です。これは意図されたものではないように見えますが、paramに対して明示的な型を提供することで回避できます。

object in JSDoc is No Longer any Under noImplicitAny

歴史的には、JavaScript のチェックに対する TypeScript のサポートは、とっつきやすい経験を提供するために、特定の方法では緩いものでした。

例えば、"幾らかのオブジェクト、それが何かを知らない"を意味する、JSDoc 内のobjectをユーザーはしばしば利用しましたが、それはanyとして扱われてきました。

// @ts-check

/**
 * @param thing {Object} some object, i dunno what
 */
function doSomething(thing) {
    let x = thing.x;
    let y = thing.y;
    thing();
}

これは、それを TypeScript のObject型として扱うことがコード内では興味のないエラーを報告することになったことが原因であり、Object型がtoStringvalueOfのようなメソッド以外のわずかな能力と共に極端に曖昧な型である時からずっとです。

しかし、TypeScript はobject(ローワーケースのoであることに気づいてください)と名付けられたより便利な型を持っていますobject型は、Objectよりもさらに厳しく、その中ではstring, booleanそしてnumberのような全てのプリミティブ型を拒絶します。残念ながら、Objectobjectの両方は、JSDoc 中ではanyとして扱われていました。

objectが重宝され、JSDoc 中でObjectよりもかなり少ない頻度で使われていたために、noImplicitAnyを使った際は JavaScript ファイル内で特別扱いの振る舞いを削除してきましたが、その結果、JSDoc 内ではobject型はまさにそのノンプリミティブなobject型を参照します。

What's Next?

我々は次のバージョン TypeScript 3.9 が、2020 年の 5 月半ばに登場し、そのほとんどがパフォーマンス、洗練、そしてPromiseに対する本質的によりスマートな型チェックに焦点を当てるだろうと予測しています。来る日に我々の計画書は詳細のアイデアを提供するために公開されるでしょう。

しかし、3.9 までずっと待たないでください。つまり 3.8 は多くの素晴らしい戒厳に伴った非常に良いリリースですので、今日それを手に入れてください!

楽しんで、そして幸せなハッキングを!

- Daniel Rosenwasser and TypeScript Team

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TypeScript 3.8 の発表記事を和訳してみた

TypeScript 3.8 で発表された機能を理解するため、発表記事を和訳しました。原文はこちら


TypeScript 3.8 は多くの新機能をもたらし、中には新しいものや、まもなく公開される ECMAScript 標準機能、型に限定した import/export のための新しいシンタックス、そしてさらに多くのものがあります。

Index

Type-Only Imports and Exports

この機能は、ほとんどのユーザーは考える必要はないかもしれません。しかし、もしあなたがここでの問題に直面していたら、それは興味の対象かもしれません(特に --isolatedModulestranspileModule API、Babel の下でのコンパイル時)。

TypeScript は JavaScript の import シンタックスを再利用し、我々は型を参照することができます。例えば次の例では、JavaScript の値である doThing を、純粋な TypeScript の型である Options と共に import できます。

// ./foo.ts
interface Options {
    // ...
}

export function doThing(options: Options) {
    // ...
}

// ./bar.ts
import { doThing, Options } from "./foo.js";

function doThingBetter(options: Options) {
    // do something twice as good
    doThing(options);
    doThing(options);
}

これは便利です。なぜなら、import する時のほとんどは、何が import されたかを心配する必要がないからです。ただ、何かを import しているというだけです。

残念ながら、これは import elision と呼ばれる機能のために機能していただけでした。TypeScript が JavaScript ファイルを出力する時、Optionsが型としてのみ使用されるのを確認し、自動的にその import を削除します。その結果の出力は、このように見えるでしょう。

// ./foo.js
export function doThing(options: Options) {
    // ...
}

// ./bar.js
import { doThing } from "./foo.js";

function doThingBetter(options: Options) {
    // do something twice as good
    doThing(options);
    doThing(options);
}

改めて、この振る舞いは通常は素晴らしいですが、幾らか他の問題を引き起こします。

まず最初に、値や型が export されているかどうかが曖昧ないくつかの場所があります。例えば、次の例におけるMyThingは、値と型のどちらでしょうか。

import { MyThing } from "./some-module.js";

export { MyThing };

ちょうどこのファイルだけに限定すれば、知る由もありません。Babel と TypeSrcipt の transpileModule API の両方は、もし MyThing が型のみであるならば正しく機能しないコードを生成するでしょうし、TypeScript の isolatedModule フラグはそれが問題になることを警告するでしょう。本当の問題は、"いやいや、本当は私はちょうど型を指したんだよ、つまりこれは削除されるべきだ"と主張する方法がないことであり、そのために import elision はいまいちなのです。

他の問題は、TypeScript import elision が、型として使われた import だけが含まれた import 文を取り除くつもりであることでした。それは副作用を持つモジュール郡に対して目立った異なる振る舞いを引き起こし、そのためにユーザーは副作用を完璧に保証する二つ目の import 文を挿入しなければならなかったでしょう。

// This statement will get erased because of import elision.
import { SomeTypeFoo, SomeOtherTypeBar } from "./module-with-side-effects";

// This statement always sticks around.
import "./module-with-side-effects";

これが現れるのを見た具体的な場所は、Angular.js (1.x)のようなフレームワーク内であり、(副作用である)service がグローバルに登録される必要があり、しかしそれらの service は型に対してただ import されたものでした。

// ./service.ts
export class Service {
    // ...
}
register("globalServiceId", Service);

// ./consumer.ts
import { Service } from "./service.js";

inject("globalServiceId", function (service: Service) {
    // do stuff with Service
});

結果として、./service.jsは、一度も実行されることはなく、実行時に壊れるでしょう。

この類の問題を避けるために、種々のものがどのようにインポート/削除されていたかに関する、よりきめの細かい制御をユーザーに提供すべきだと気付きました。

TypeScript 3.8 における解決策として、型だけの import と export に対する新しいシンタックスを追加しました。

import type { SomeThing } from "./some-module.js";

export type { SomeThing };

import typeは、型アノテーションと宣言に対して使用される宣言だけをインポートします。それはいつも完全に削除された状態で、よって実行時にはそれの残存物はありません。同じように、export typeは型コンテキストに対して使用されうる export を提供するだけで、それもまた TypeScript の出力からは削除されます。

実行時に値を、設計時に型をクラスはもち、その利用はコンテキストの影響を受けることに注意してください。クラスをインポートするためにimport typeを使用する時、それから継承するようなことはできません。

import type { Component } from "react";

interface ButtonProps {
    // ...
}

class Button extends Component<ButtonProps> {
    //               ~~~~~~~~~
    // error! 'Component' only refers to a type, but is being used as a value here.

    // ...
}

もし以前のフローを使用してきたなら、そのシンタックスはかなり似ています。一つの違いは、不明瞭に見えたかもしれないコードを避けるために少しの制限を追加したことです。

// Is only 'Foo' a type? Or every declaration in the import?
// We just give an error because it's not clear.

import type Foo, { Bar, Baz } from "some-module";
//     ~~~~~~~~~~~~~~~~~~~~~~
// error! A type-only import can specify a default import or named bindings, but not both.

import typeと共に、実行時に利用されないであろう import に伴って発生することを制御するための、新しいコンパイラのフラグ importsNotUsedAsValues も追加しました。このフラグは 3 つの異なる値を取ります

  • remove:これはこれらの import を削除する現在の振る舞いです。デフォルトであり続けることになっており、破壊的変更ではありません。
  • preserve:これは一度も使われていない値を持つ全ての import を保存します。これは、import/副作用を保存させることができます。
  • error:これは全ての import を保存します(preserveオプションと全く同じです)が、値の import が型としてのみ使用されたときにエラーとなるでしょう。どの値も思わず import されていないことを保証したい時にこれは便利だったかもしれませんが、import に明示的な副作用を今もなおもたらします。

この機能についての更なる情報は、pull requestや、import type宣言からの import が使用されうる場所の周辺での関連した変更から確認できます。

ECMAScript Private Fields

TypeScript 3.8 は、stage-3 class fields proposalの一部である、ECMAScript の private フィールドに対するサポートをもたらします。この仕事は、Bloomberg での良き友達によって開始され、完成まで突き動かされました。

class Person {
    #name: string

    constructor(name: string) {
        this.#name = name;
    }

    greet() {
        console.log(`Hello, my name is ${this.#name}!`);
    }
}

let jeremy = new Person("Jeremy Bearimy");

jeremy.#name
//     ~~~~~
// Property '#name' is not accessible outside class 'Person'
// because it has a private identifier.

通常のプロパティとは(private修飾子と共に宣言されるものでさえ)違って、private フィールドは覚えておくべき少しのルールを持っています。それらのいくつかは:

  • private フィールドは#文字から始まります。時々、これらを private names と呼びます。
  • 全ての private フィールド名は、それを含むクラスの領域で一意です。
  • publicあるいはprivateのような TypeScript のアクセス修飾子は、private フィールド上では使用できません。
  • private フィールドは、それが含まれるクラスの外側では(JS ユーザーによってでさえも)アクセスあるいは発見さえできません。時々、これを hard privacy と呼びます。

"hard" privacy から離れて、private フィールドのもう一つの利点は、ちょうど言及した一意性です。例えば、通常のプロパティ宣言は、サブクラスにおいて上書きされる傾向があります。

class C {
    foo = 10;

    cHelper() {
        return this.foo;
    }
}

class D extends C {
    foo = 20;

    dHelper() {
        return this.foo;
    }
}

let instance = new D();
// 'this.foo' refers to the same property on each instance.
console.log(instance.cHelper()); // prints '20'
console.log(instance.dHelper()); // prints '20'

private フィールドに伴い、これについて心配する必要はなく、というのもそれぞれのフィールド名はそのクラスで一意であるためです。

class C {
    #foo = 10;

    cHelper() {
        return this.#foo;
    }
}

class D extends C {
    #foo = 20;

    dHelper() {
        return this.#foo;
    }
}

let instance = new D();
// 'this.#foo' refers to a different field within each class.
console.log(instance.cHelper()); // prints '10'
console.log(instance.dHelper()); // prints '20'

なきに等しいもう一つのことは、その他の型で private フィールドにアクセスすると、TypeError をもたらすであろうことです!

class Square {
    #sideLength: number;

    constructor(sideLength: number) {
        this.#sideLength = sideLength;
    }

    equals(other: any) {
        return this.#sideLength === other.#sideLength;
    }
}

const a = new Square(100);
const b = { sideLength: 100 };

// Boom!
// TypeError: attempted to get private field on non-instance
// This fails because 'b' is not an instance of 'Square'.
console.log(a.equals(b));

最後に、いかなる単純な.jsファイルユーザーに対し、private フィールドは常にそれらが割り当てられる前に宣言されなければなりません。

class C {
    // No declaration for '#foo'
    // :(

    constructor(foo: number) {
        // SyntaxError!
        // '#foo' needs to be declared before writing to it.
        this.#foo = foo;
    }
}

TypeScript はクラスのプロパティに対していつも宣言を要求してきたのに対して、JavaScript はいつも、宣言されていないプロパティにユーザーがアクセスすることを許してきました。private フィールドに伴い、.jsあるいは.tsファイルのどちらに取り組んでいるかに関係なく、宣言が常に要求されます。

class C {
    /** @type {number} */
    #foo;

    constructor(foo: number) {
        // This works.
        this.#foo = foo;
    }
}

この実装についての更なる情報は、元の pull requestから確認できます。

Which should I use?

TypeScript ユーザーとして private のどちらの種類を使用すべきかについてのたくさんの質問を受け取ってきました。つまり一般的には、"privateキーワードを使うべきか、あるいは ECMAScript の hash/pound (#) private フィールドを使うべきか"です。

全ての良い質問に対し、その回答は良いものではありません。それは場合によるのです!

プロパティに関しては、TypeScript の private 修飾子は常に削除されます(すなわち、実行時にそれは全体的に普通のプロパティのように振る舞い、そしてそれが private 修飾子によって宣言されたことを伝える手段はありません)。private キーワードを使用する時、privacy はコンパイル時/設計時においてのみ強制され、JavaScript コードを使用する者に対してそれは全体的に意図によるものです。

class C {
    private foo = 10;
}

// This is an error at compile time,
// but when TypeScript outputs .js files,
// it'll run fine and print '10'.
console.log(new C().foo);    // prints '10'
//                  ~~~
// error! Property 'foo' is private and only accessible within class 'C'.

// TypeScript allows this at compile-time
// as a "work-around" to avoid the error.
console.log(new C()["foo"]); // prints '10'

上側は、"soft privacy"のこの類が、幾らかの API に対するアクセスを持たないことをコードの使用者が一時的に回避する手助けをし、そしてまた、どんな実行時においても機能します。

一方で、ECMAScript の#private は、そのクラスの外側からは完全にアクセス不可能です。

class C {
    #foo = 10;
}

console.log(new C().#foo); // SyntaxError
//                  ~~~~
// TypeScript reports an error *and*
// this won't work at runtime!

console.log(new C()["#foo"]); // prints undefined
//          ~~~~~~~~~~~~~~~
// TypeScript reports an error under 'noImplicitAny',
// and this prints 'undefined'.

この hard privacy は、内部のいかなるものを誰も利用できないことを厳密に保証する上では本当に便利です。もしライブラリの作者なら、private フィールドの削除あるいは改名は、決して破壊的変更をもたらすべきではありません。

上述したように、もう一つの利点は、ECMAScript の#privates に伴ってサブクラス化が容易であることであり、なぜならそれらは本当にprivate であるからです。ECMAScript#private フィールドを使用する時、どのサブクラスも今までフィールド名の衝突を心配したことはありません。TypeScript の private プロパティ宣言に関しては、ユーザーは今もなおスーパークラスで宣言されたプロパティを再び宣言しないように気をつけなければなりません。

考慮すべきもう一つの事項は、コードをどこで実行するつもりなのかです。TypeScript は現在、ECMAScript 2015 (ES6)あるいはそれ以上の target を標的としない限り、この機能をサポートできません。これは、privacy を強制するために基準を下げた実装が WeakMap を使用しており、WeakMap はメモリリークを引き起こさない方法では polyfill され得ないからです。対照的に、TypeScript の private 宣言されたプロパティは、全ての target で(ECMAScript 3 でさえ)機能します。

最後の考慮は速さだったかもしれません。つまり、private プロパティは他のプロパティと違いがなく、そのためそれらへのアクセスは、どの runtime を target としていようとも、他のプロパティアクセスと同じくらい速いです。対照的に、#private フィールドは WeakMap を使って基準が下げられているため、使用するにはより遅いかもしれません。幾らかの runtime は#private フィールドの実際の実装を最適化し、速度の速い WeakMap の実装さえ持っていたかもしれない一方で、全ての runtime ではそうでなかったかもしれません。

export * as ns Syntax

単一のメンバーとしてもう一つのモジュールの全てのメンバーを公開するような、単一のエントリーポイントを持つことは、しばしば一般的です。

import * as utilities from "./utilities.js";
export { utilities };

これはとても一般的なので、ECMAScript 2020 は最近、このパターンをサポートする新しいシンタックスを追加しました。

export * as utilities from "./utilities.js";

これは JavaScript に対して良い QOL の改善であり、そして TypeScript 3.8 はこのシンタックスを実装しています。モジュールの target がes2020よりも以前であるとき、TypeScript は最初のコードスニペットに沿って何かしらを出力するでしょう。

この機能を実装したコミュニティメンバーであるWenlu Wang (Kingwl)氏には特に感謝いたします。更なる情報は、元の pull requestをご確認ください。

Top-Level await

JavaScript の中で(HTTP リクエストのような)I/O を提供するほとんどのモダンな環境は非同期であり、多くのモダンな API は Promise を返します。これはノンブロッキングな操作を作る上で多くの利点を持つ一方で、ファイルあるいは外部のコンテンツのロードのようなものを驚くほど面倒にします。

fetch("...")
    .then(response => response.text())
    .then(greeting => { console.log(greeting) });

Promise に伴う.thenチェインを避けるため、JavaScript ユーザーはawaitを使用するためにasyncfunction をしばしば導入し、そしてそれを定義した後でその function をすぐさま呼んでいました。

async function main() {
    const response = await fetch("...");
    const greeting = await response.text();
    console.log(greeting);
}

main()
    .catch(e => console.error(e))

asyncfunction の導入を避けるため、近く発表される ECMAScript の使いやすい機能、"top-level await"を使用することができます。

以前は JavaScript において(似た機能を伴う他のほとんどの言語に沿って)、awaitasyncfunction の本文中でのみ許可されていました。しかし、top-level await によって、awaitをモジュールのトップレベルで使用することができます。

const response = await fetch("...");
const greeting = await response.text();
console.log(greeting);

// Make sure we're a module
export {};

繊細さがあることの注記:top-level await はモジュールのトップレベルでのみ機能し、TypeScript がimportあるいはexportを見つけるとき、ファイルはモジュールとしてのみ見なされます。幾らかの基本的なケースにおいて、これを保証するためにexport {}を幾らかの定型として書き出す必要があったかもしれません。

top level await は、現時点であなたが期待する全ての環境では機能しないかもしれません。現在、target コンパイラオプションがes2017かそれ以上で、moduleexnextあるいはsystemであるときのみ、top level await を使用できます。いくつかの環境とバンドラーの中でのサポートは、制限されるかもしれず、あるいは実験的サポートを有効にすることを要求するかもしれません。

実装の更なる情報は、元の pull requestをご確認ください。

es2020 for target and module

Kagami Sascha Rosylight (saschanaz)のおかげで、TypeScript 3.8 はes2020moduletargetに対するオプションとしてサポートします。これは、optional chaining, nullish coalescing, export * as ns, そして動的import(...)シンタックスのようなより新しい ECMAScript 2020 の機能を保存するでしょう。それはまた、bigintリテラルが今やexnext配下で安定した target を持つことを意味します。

JSDoc Property Modifiers

TypeScript 3.8 はallowJsフラグをオンにすることで JavaScript ファイルをサポートし、またcheckJsオプションか// @ts-checkコメントを.jsファイルのトップに追加することによって、それらの JavaScript ファイルのtype-checkingをサポートします。

JavaScript ファイルは型チェックに対する熱心なシンタックスを持たないために、TypeScript は JSDoc を利用します。TypeScript 3.8 はプロパティに対する少しばかりの新しい JSDoc タグを理解します。

最初はアクセス修飾子です。つまり@public, @private, @protectedです。これらのタグは TypeScript 内でそれぞれ機能するpublic, private, protectedと全く同じように機能します。

// @ts-check

class Foo {
    constructor() {
        /** @private */
        this.stuff = 100;
    }

    printStuff() {
        console.log(this.stuff);
    }
}

new Foo().stuff;
//        ~~~~~
// error! Property 'stuff' is private and only accessible within class 'Foo'.
  • @publicはいつも暗に意味され、省略できますが、プロパティがどこからでも参照されることを意味します。
  • @privateは、プロパティがそれを含むクラス内でしか利用できないことを意味します。
  • @protectedは、プロパティがそれを含むクラスと全ての生成されたサブクラス内で利用でき、それを含むクラスの似ていないインスタンス上では利用できないことを意味します。

次に、プロパティが初期化中にのみ書かれることを保証するための修飾子@readonlyを追加しました。

// @ts-check

class Foo {
    constructor() {
        /** @readonly */
        this.stuff = 100;
    }

    writeToStuff() {
        this.stuff = 200;
        //   ~~~~~
        // Cannot assign to 'stuff' because it is a read-only property.
    }
}

new Foo().stuff++;
//        ~~~~~
// Cannot assign to 'stuff' because it is a read-only property.

Better Directory Watching on Linux and watchOptions

TypeScript 3.8 はディレクトリのウォッチに対する(node_modulesに対する変更を効率的にピックアップするのに重要な)新しい戦略を生み出します。

幾らかのコンテキストに対して、Linux のようなオペレーティングシステム上では、TypeScript は依存性における変更を発見するために、node_modules上に(ファイルウォッチャーとは対照的な)ディレクトリウォッチャーとそのサブディレクトリの多くをインストールします。これは、より少ないディレクトリを追跡するための方法が存在する一方で、利用可能なファイルウォッチャーの数がしばしばnode_modules内のファイルによって覆い隠されるためです。

TypeScript のより古いバージョンは、フォルダー上にディレクトリウォッチャーをすぐさまインストールしたでしょうし、起動時はうまくいっていたでしょう。しかし、npm インストールの間、多くの処理がnode_modules内で実行され、それは TypeScript を圧倒しうることもあり、しばしばエディターのセッションを鈍くします。これを防ぐため、これらのかなり不安定なディレクトリが安定するための幾らかの時間を提供するために、TypeScript 3.8 はディレクトリウォッチャーのインストールまでわずかに待ちます。

全てのプロジェクトが異なる戦略下でよりうまく機能したかもしれず、そしてこの新しいアプローチがあなたのワークフローに対して機能しないであろうことを理由に、ユーザーが戦略をウォッチしているコンパイラ/言語サービスがファイルとディレクトリの経過を追うために使用されるべきであることを伝えられる新しいwatchOptionsフィールドを、TypeScript 3.8 はtsconfig.jsonjsconfig.jsonに導入します。

{
    // Some typical compiler options
    "compilerOptions": {
        "target": "es2020",
        "moduleResolution": "node",
        // ...
    },

    // NEW: Options for file/directory watching
    "watchOptions": {
        // Use native file system events for files and directories
        "watchFile": "useFsEvents",
        "watchDirectory": "useFsEvents",

        // Poll files for updates more frequently
        // when they're updated a lot.
        "fallbackPolling": "dynamicPriority"
    }
}

watchOptionsは設定できる 4 つの新しいオプションを含みます。

  • watchFile:個々のファイルがどのようにウォッチされるかの戦略。次の値を取ることができます。
    • fixedPollingInterval:一定間隔で一秒に数回、変更に対して全てのファイルをチェックします。
    • priorityPollingInterval:一秒に数回、変更に対して全てのファイルをチェックしますが、経験則を用いてファイルのある型を他よりもより少ない頻度でチェックします。
    • dynamicPriorityPolling:動的キューを用いて、より修正頻度の少ないファイルがより少ない回数でチェックされます。
    • useFsEvents:(デフォルト)ファイルの変更に対してオペレーティングシステム/ファイルシステムのネイティブイベントの利用を試みます。
    • useFsEventsOnParentDirectory:ディレクトリを含むディレクトリ上での変更をリッスンするため、オペレーティングシステム/ファイルシステムのネイティブイベントの利用を試みます。これはより少ないファイルウォッチャーを使用することができますが、より正確ではなくなったかもしれません。
  • watchDirectory:ディレクトリツリー全体が再帰的なファイルウォッチ機能に欠けたシステム化でどのようにウォッチされるかの戦略。次の値を取ることができます。
    • fixedPollingInterval:一定間隔で一秒に数回、変更に対して全てのディレクトリをチェックします。
    • dynamicPriorityPolling:動的キューを用いて、より修正頻度の少ないディレクトリがより少ない回数でチェックされます。
    • useFsEvents:(デフォルト)ディレクトリの変更に対してオペレーティングシステム/ファイルシステムのネイティブイベントの利用を試みます。
  • fallbackPolling:ファイルシステムイベントの利用時、システムがネイティブのファイルウォッチャーを持っていない、そして/あるいはサポートしていないときに使われる投票戦略をこのオプションは指定します。次の値を取ることができます。
    • fixedPollingInterval:(上述)
    • priorityPollingInterval:(上述)
    • dynamicPriorityPolling:(上述)
  • synchronousWatchDirectory:ディレクトリ上での遅延ウォッチを無効化します。遅延ウォッチは多くのファイルの変更が一度に発生するとき(例えばnpm installの実行時のnode_modules内での変更)に便利ですが、幾らかより一般的でない設定のために、このフラグによってそれを無効化したかったかもしれません。

これらの変更における更なる情報は、GitHub で pull requestを参照ください。

"Fast and Loose" Incremental Checking

TypeScript の--watchモードと--incrementalモードは、プロジェクトに対してフィードバックループを厳しくする手助けが可能です。--incrementalモードをオンにすることは、TypeScript にどのファイルが他に影響を与えうるかを追跡させることができ、それに加えて、--watchモードはコンパイラプロセスをオープンに保ち、可能な限りの量のメモリ内にある情報を再利用します。

しかしながら、もっと大きなプロジェクトに対しては、これらのオプションが我々に対して余裕のある速度での劇的な利益でさえ充分ではありません。例えば Visual Studio Code チームは、そのウォッチモード内で再チェック/再ビルドされる必要のあったファイルを査定する上ではより不正確だったであろう、gulp-tsbと呼ばれる TypeScript 周りでの彼ら独自のビルドツールを開発してきましたが、その結果、より極端に短いビルド時間を提供することができました。

ビルド速度のために正確さを犠牲にすることは、良くも悪くも、TypeScript/JavaScript の世界で多くの開発者が進んで作ろうとすることのトレードオフです。多くのユーザーは、エラーに対処することよりも彼らのイテレーションの時間を短くすることを前もって優先します。例として、型チェックや lint の結果にかかわらず、コードをビルドすることは極めて一般的です。

TypeScript 3.8 は新しいコンパイラオプションassumeChangesOnlyAffectDirectDependenciesを導入します。このオプションが有効化されたとき、TypeScript は本当に影響を受けた可能性のある全てのファイルを再チェック/再ビルドすることを避け、それらを直接インポートするファイルだけでなく、変更したファイルの再チェック/再ビルドだけを行うでしょう。

例えば、次のようにfileA.tsをインポートするfileB.tsを、インポートするfileC.tsを、インポートするfileD.tsファイルを考えてみてください。

fileA.ts <- fileB.ts <- fileC.ts <- fileD.ts

--watchモードでは、fileA.tsにおける変更は典型的には、TypeScript がfileB.ts, fileC.tsそしてfileD.tsの再チェックを少なくとも必要とすることを意味したでしょう。assumeChangesOnlyAffectDirectDependenciesの下では、fileA.tsにおける変更は、fileA.tsfileB.tsだけ再チェックされる必要があることを意味します。

Visual Studio Code のようなコードベースでは、これは特定のファイル内での変更に対する再ビルド時間をおよそ 14 秒からおよそ 1 秒に短縮します。全てのコードベースに対してこのオプションを必ずしもおすすめしませんが、極端に大きなコードベースをもち、後までずっとプロジェクト全体のエラーを先送りにしたい場合(例えば、tsconfig.fullbuild.json経由あるいは CI 内での特化したビルド)は、興味があったかもしれません。

更なる詳細は、元の pull requestをご確認ください。

Editor Features

Convert to Template String

Arooran Thanabalasingam (bigaru)のおかげで、TypeScript 3.8 は新しいリファクタリングを生み出し、次のような文字列の連結を

"I have " + numApples + " apples"

次のようなテンプレート文字列に変換します。

`I have ${numApples} apples`

convert-to-template-string

Call Hierarchy

与えられたファンクションの呼び出し元を把握するのはしばしば便利です。TypeScript は宣言の全ての参照を見つける方法をもっており(すなわちFind All Referencesコマンド)、ほとんどの人々はその質問に答えるためにそれを利用できます。しかし、それはわずかに厄介になり得ます。例えば、fooと名付けられたファクションの呼び出し元を探し出そうとすることを想像してください。

export function foo() {
    // ...
}

// later, much farther away from 'foo'...
export function bar() {
    foo();
}

export function baz() {
    foo()
}

foobarbazによって呼び出されたのだと発見すると、同様にbarbazも呼び出し元を知りたくなります!そのようにして、barbazに対しても同様にFind All Referencesを行使することができますが、元々答えようとしていた質問である「fooの呼び出し元は何か」のコンテキストを失います。

ここでのその制約に対処するため、幾らかのエディタはShow Call Hierarchyと呼ばれるコマンドを通じてファンクションが呼び出される経路を可視化する機能を持っており、TypeScript 3.8 は公式にCall Hierarchy機能をサポートします。

call-hierarchy-pic

Call Hierarchyは、最初は少しややこしかったかもしれませんし、その周辺の直感を築き上げるための利用法を必要とします。次のコードを考えてみましょう。

function frequentlyCalledFunction() {
    // do something useful
}

function callerA() {
    frequentlyCalledFunction();
}

function callerB() {
    callerA();
}

function callerC() {
    frequentlyCalledFunction();
}

function entryPoint() {
    callerA();
    callerB();
    callerC();
}

次のテキストのツリー図は、frequentlyCalledFunctionの call hierarchy を示します。

frequentlyCalledFunction

├─callerA
│ ├─ callerB
│ │ └─ entryPoint
│ │
│ └─ entryPoint

└─ callerC
└─ entryPoint

ここでは、frequentlyCalledFunctionの直近の呼び出し元がcallerAcallerBであることが見て取れます。callerAを呼び出すのは何かを知りたい場合、callerBと呼ばれるファンクションに沿って、プログラムのエントリーポイントがそれを直に呼び出すことを確認できます。callerBcallerCの呼び出し元をさらに展開して、それらがentryPointファンクション内でのみ呼ばれていることを確認できます。

Call HierarchyVisual Studio Code Insiders内の TypeScript/JavaScript に対して既にサポートされており、次の stable バージョンで利用可能になるでしょう。

call-hierarchy-gif

Breaking Changes

TypeScript 3.8 は、注記すべき少しのマイナーな破壊的変更を含んでいます。

Stricter Assignability Checks to Unions with Index Signatures

以前は、ユニオン型に割り当てられているときは excess プロパティはチェックされませんでした(たとえ、その excess プロパティがその index シのグネチャを一度も満たすことがなかったとしても)。TypeScript 3.8 では、型チェックがより厳しくなり、あるプロパティがインデックスのシグネチャをもっともらしく満たせる場合にのみ、そのプロパティは excess プロパティチェックを免除されます。

const obj1: { [x: string]: number } | { a: number };

obj1 = { a: 5, c: 'abc' }
//             ~
// Error!
// The type '{ [x: string]: number }' no longer exempts 'c'
// from excess property checks on '{ a: number }'.

let obj2: { [x: string]: number } | { [x: number]: number };

obj2 = { a: 'abc' };
//       ~
// Error!
// The types '{ [x: string]: number }' and '{ [x: number]: number }' no longer exempts 'a'
// from excess property checks against '{ [x: number]: number }',
// and it *is* sort of an excess property because 'a' isn't a numeric property name.
// This one is more subtle.

Optional Arguments with no Inferences are Correctly Marked as Implicitly any

次のコードでは、現在paramnoImplicitAnyのもとでエラーと共にマークされています。

function foo(f: () => void) {
    // ...
}

foo((param?) => {
    // ...
});

これは、foo中のfの型に対して一致するパラメータがないことが原因です。これは意図されたものではないように見えますが、paramに対して明示的な型を提供することで回避できます。

object in JSDoc is No Longer any Under noImplicitAny

歴史的には、JavaScript のチェックに対する TypeScript のサポートは、とっつきやすい経験を提供するために、特定の方法では緩いものでした。

例えば、"幾らかのオブジェクト、それが何かを知らない"を意味する、JSDoc 内のobjectをユーザーはしばしば利用しましたが、それはanyとして扱われてきました。

// @ts-check

/**
 * @param thing {Object} some object, i dunno what
 */
function doSomething(thing) {
    let x = thing.x;
    let y = thing.y;
    thing();
}

これは、それを TypeScript のObject型として扱うことがコード内では興味のないエラーを報告することになったことが原因であり、Object型がtoStringvalueOfのようなメソッド以外のわずかな能力と共に極端に曖昧な型である時からずっとです。

しかし、TypeScript はobject(ローワーケースのoであることに気づいてください)と名付けられたより便利な型を持っていますobject型は、Objectよりもさらに厳しく、その中ではstring, booleanそしてnumberのような全てのプリミティブ型を拒絶します。残念ながら、Objectobjectの両方は、JSDoc 中ではanyとして扱われていました。

objectが重宝され、JSDoc 中でObjectよりもかなり少ない頻度で使われていたために、noImplicitAnyを使った際は JavaScript ファイル内で特別扱いの振る舞いを削除してきましたが、その結果、JSDoc 内ではobject型はまさにそのノンプリミティブなobject型を参照します。

What's Next?

我々は次のバージョン TypeScript 3.9 が、2020 年の 5 月半ばに登場し、そのほとんどがパフォーマンス、洗練、そしてPromiseに対する本質的によりスマートな型チェックに焦点を当てるだろうと予測しています。来る日に我々の計画書は詳細のアイデアを提供するために公開されるでしょう。

しかし、3.9 までずっと待たないでください。つまり 3.8 は多くの素晴らしい戒厳に伴った非常に良いリリースですので、今日それを手に入れてください!

楽しんで、そして幸せなハッキングを!

- Daniel Rosenwasser and TypeScript Team

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「何か食べたいけど何が食べたいのか分からない」あなたへ

きょう何食べようか悩んでいるあなたへ

5つの質問に答えるだけで、料理ジャンルをサジェストしてくれるサービスを公開しました :hugging:
https://kyounanitaberu.appspot.com/
スクリーンショット 2020-02-23 11.03.41.png スクリーンショット 2020-02-23 11.03.22.png

きっかけ

スクリーンショット 2020-02-23 2.59.29.png
きっかけは一つの動画。
私は美味しいものを食べることが大好きなんですが、好きが故に何を食べるかめちゃくちゃ悩んで時間を消費してしまいます。
この動画を見て「同じことを思っている人がいるんだ」と気づけたので
2択で今の気分を選ぶと、料理ジャンルをサジェストしてくれるWebアプリケーションを作りました。

システム

システムとしてはVue.jsで作ったアプリをGCP(GoogleCloudPlatform)にデプロイしているちょーシンプルな構成です。

画面構成も”NavBarコンポーネント”と”Cardコンポーネント”と至って簡単。

image.png
↓ファイル構成はこんな感じ。

$ tree -L 2 --matchdirs src
src
├── App.vue
├── components
│   ├── Card.vue
│   ├── NavBar.vue
│   ├── Search.vue ←食べログとUberEatsのリンク
│   └── Share.vue ←SNSのリンク
├── data
│   ├── questions.js ←”質問・回答”と”次の質問・回答”or”サジェストする料理ジャンルID”のマッピングファイル
│   └── results.js ←”料理ジャンルID”と”ジャンル名や画像”とのマッピングファイル
├── main.js
├── store
│   ├── actions.js
│   ├── index.js
│   └── mutations.js
└── views
    └── Main.vue

ちなみに表示するカードの切り替えにはVuex、UIライブラリにはBootstrapを使っているので、Vueを勉強したての人が一通り復習するのにいいかも、と思いました :thinking:

また、GCPへのデプロイは、Vue.jsで作成したSPAなアプリをGoogle App Engineへデプロイするを参考にさせていただきました、ありがとうございます。

感想

昨日さっそく使ってみたのですが、いつも30分くらい悩むところを(悩みすぎ)5分くらいでパパッと決められて満足です。

改善点は色々ありますが、質問や回答を自分で考えてファイルとして定義しているので、こういうところで機械学習を使っていきたいなーと思いました。

最後に、私はVueもGCPも初心者ですが1〜2日(ずっと開発してたわけではない)でゼロから公開までできました。
VueやGCPに興味をお持ちの方に「このくらいのものは作れるんだ」と思ってもらえると嬉しいです :thumbsup:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

花粉症対策デジタル医療相談Botの開発 ユーザーIDと位置情報をFirestoreで管理

概要

耳鼻咽喉科の開業医をしながらデジタルテクノロジーを使った医療の効率化や患者さん向けサービスの開発研究を行っています。

スギ花粉の飛散量が増えてきました。花粉症の方にはつらい季節ですね。
忙しくて医療機関を受診できなかったり、新型コロナウイルスが心配で受診を控えている方も多いのではないでしょうか?

最近薬局や通販で購入できる医療用医薬品(医療機関で処方されるものと同成分)が増えてきたのはご存じでしょうか?これらの薬を上手に利用できれば医療機関を受診できなくても花粉シーズンを乗り越えることが出来るかもしれません。

上手に利用するには自分の花粉症状がどの程度重症なのかや、利用しようとする薬の特性を知っていないといけませんが、その辺を教えてくれるサービスがなかったので作成してみました。現在(2020年2月19日~3月4日)クラウドファンディングプラットフォームCAMPFIREでテスト版ユーザー募集しています。プロジェクトはサクセスしましたが、たくさんの花粉症の方に使って頂いて、サービス向上のためご意見ご感想をいただきたいと思っています。

CAMPFIREのプロジェクトページはこちら
LINEで花粉症の重症度や最適な市販薬がわかるデジタル医療相談【アレルナビ】

このサービスではユーザーが特定した地点のピンポイント花粉飛散予測を返す機能があります。ユーザーから送っていただいた位置情報とLINE IDはFirestoreで管理しましたのでその辺りをまとめました。

FirestoreのDatabase

・コレクションに位置情報をまとめた「locations」とLINE IDをまとめた「users」が作成されています。
・コレクション「locations」と「users」のドキュメントはユーザーがLINEを使うときに取得できるidで紐づけられています。
・コレクション「locations」のフィールドはユーザーから位置情報が送られてくるたびに更新されます(latitude緯度、longitude経度)。
・コレクション「users」のフィールドはユーザーのLINE IDが入ります。
image.png

image.png

作成方法

1. Firebaseで新規プロジェクトを作成
・Googleにログインしている状態で、Firebase公式ページの右上にある「コンソールへ移動」ボタンから、ユーザーページに移動。
・「プロジェクトを追加」から新規プロジェクトを作成。

2. Firestoreを作成
・プロジェクトメインページ左のメニューバーから「Database」を選び、「データベースの作成」に進む。
・「テストモードで開始」を選択し、「有効にする」をクリックしデータベースを作成。

3. Firebaseとnode.jsで開発したアプリを連携
・Firebaseのプロジェクトのメインページから、「アプリを追加」→「ウェブ」に進む。
・任意のアプリ名を入力し、「アプリを登録」をクリックし連携に必要なコードを表示する。

4. Firebase SDK を追加して Firebase を初期化
こちらを参考にしました。
Firebase を JavaScript プロジェクトに追加する

5. プログラム作成
ユーザーからメッセージが来たらユーザーIDが登録されているかを判定
登録されてなければFirebaseに登録

 let userRef = db.collection('users');
  let snapshot = await userRef.where("line_user_id", "==", event.source.userId).get();
  let user_id = "";
  if (snapshot.empty) {
    user_id = await userRef.add({
      line_user_id: event.source.userId
    }).then(ref => ref.id);
  } else {
    user_id = snapshot.docs[0].id;
  }
  console.log(user_id);

位置情報が送られてきたらFirebaseの位置情報をidに紐づけて更新し
「位置情報が登録されました」をユーザーに返す

 if (event.message.type === "location") { 
    client.replyMessage(event.replyToken, {
      type: 'text',
      text: "位置情報が登録されました。"
    });
    let locationsRef = db.collection('locations').doc(user_id);   
    let setAda = locationsRef.set({
      latitude:event.message.latitude,
      longitude: event.message.longitude,     
    });      
    return Promise.resolve(null);
  } 

完成図

ピンポイント花粉情報がユーザーに返されます
IMG-0997.PNG

考察

最初はユーザーから送られるすべてのIDと位置情報をFirebaseに登録しif文で取得していたため、ユーザーが増えると処理に時間がかかりそうでした。idは重複がないように、位置情報は最新のものだけを登録できたのでスッキリして気持ちがよいですね。今後は内服薬やアレルギーの重症度、花粉飛散の実測値を登録・分析することによってユーザーの住んでいる場所の予測飛散量とユーザーの重症度から適切な治療薬を推奨できるようにしていきたいと思っています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

javascriptでしょぼい本屋を作ってみた

機能説明

  • 本の一覧表示
  • 指定した金額以下での検索
  • 指定した在庫以下での検索
  • 指定した著者名の本検索
  • 在庫の本のトータルの金額表示
  • トータルの本の在庫量表示

しょぼい本屋なのでこのくらいで勘弁してください。。

    <section id="bookslist">
      <h2>本の一覧表示</h2>
      <ul class="search">
        <li class="all">
          <p>全ての本を表示します</p>
          <button>検索</button>
        </li>

        <li class="price">
          <p>指定した金額以下を調べる</p>
          <input type="text" id="input-price">
        </li>

        <li class="quantity">
          <input type="text">
          <p>指定した在庫以下を調べる</p>
        </li>

        <li class="author">
          <p>一致した著者の本を調べる</p>
          <input type="text">
        </li>
      </ul>

      <h3>検索した結果を返す</h3>
      <div class="lists">

      </div>
    </section>

    <section id="detailbook">
      <h2>本の詳細表示</h2>
      <ul class="search">
        <li>
          <p>トータルの金額</p>
          <button class="total-num">検索</button>
        </li>

        <li>
          <p>トータルの個数</p>
          <button class="total-price">検索</button>
        </li>
      </ul>

      <div class="lists">

      </div>
    </section>

const bookShopFunc = (books) => {
  return {
    title: "タイトル",
    price: "金額",
    quantity: "在庫",
    release_date: "発売日",
    author: "著者",
    // トータルの数量を返す
    totalNum(Compare){
      return books.reduce(Compare, 0)
    },
    inputCompareValue(event, flag) {
      const InputValue = event.target.value;
      if (flag === "price"){
        return books.filter(book => InputValue >= book.price)
      }else if (flag === "quantity") {
        return books.filter(book => InputValue >= book.quantity)
      }else if (flag === "author") {
        return books.filter(book => InputValue === book.author)
      }
    },
    listBtnClick(btn, section, array, html) {
      btn.addEventListener('click', () => {
        section.textContent = null;
        if (!array.length) {
          section.insertAdjacentHTML('beforeend', bookshop.noBookHtml())
          return true;
        }
        array.forEach(book => section.insertAdjacentHTML('beforeend', html(book)))
      });
    },
    listInput (btn, section, html, flag) {
      btn.addEventListener('input', (e)=> {
        const InputValue = e.target.value;
        const array = bookshop.inputCompareValue(e, flag)
        section.textContent = null;
        if (!array.length) {
          section.insertAdjacentHTML('beforeend', bookshop.noBookHtml())
          return;
        }
        array.forEach(book => section.insertAdjacentHTML('beforeend', html(book)))
      });
    },
    targetBtnClick (btn, appendBox, target, html, key) {
      btn.addEventListener("click", () => {
        appendBox.textContent = null;
        if (target === undefined || target === null) {
          appendBox.insertAdjacentHTML('beforeend', bookshop.noBookHtml())
          return;
        }
        appendBox.insertAdjacentHTML("beforeend", html(key, target))
      });
    },
    booksListHtml(book){
      return `<div class="book">
                <p class="book-title">タイトル:${book.title}</p>
              </div>`
    },
    answertHtml(key, target) {
      console.log(key)
      return `<p class="">${key}${target}</p>`
    },
    noBookHtml() {
      return `<div class="book">
                <p class="no">本は見当たらなかったよ!</p>
              </div>`
    }
  }
}

// オブジェクトの配列を定義
const books = [
  {title: "ハリーポッター", price: 1000, quantity: 100, release_date: '2013/11/27', author: "tanaka"},
  {title: "ハレルヤ", price: 2000, quantity: 300, release_date: '2015/1/20', author: "hirata"},
  {title: "カジカジ", price: 3000, quantity: 200, release_date: '2012/5/10', author: "tanaka"},
  { title: "山が好き", price: 4000, quantity: 300, release_date: '2020/1/20', author: "otsuka"}
];

// インスタンス的な変数
const bookshop = bookShopFunc(books);

// 検索一覧用の変数
const bookList = document.querySelector('#bookslist .lists');
const listBtn = document.querySelector('.search .all button');
const priceInput = document.querySelector('.search .price input');
const quantityInput = document.querySelector('.search .quantity input');
const authorInput = document.querySelector('.search .author input');

// 検索一覧用のイベント
bookshop.listBtnClick(listBtn, bookList, books, bookshop.booksListHtml);
bookshop.listInput(priceInput, bookList, bookshop.booksListHtml, "price");
bookshop.listInput(quantityInput, bookList, bookshop.booksListHtml, "quantity");
bookshop.listInput(authorInput, bookList, bookshop.booksListHtml, "author");

//  金額や在庫数の確認
const detailBook = document.querySelector('#detailbook .lists');
const totalNumBtn = document.querySelector('#detailbook .total-num');
const totalPriceBtn = document.querySelector('#detailbook .total-price');
bookshop.targetBtnClick(totalNumBtn, detailBook, bookshop.totalNum((total, book) => { return total + book.quantity;}), bookshop.answertHtml, bookshop.quantity);
bookshop.targetBtnClick(totalPriceBtn, detailBook, bookshop.totalNum((total, book) => {return total + book.price;}), bookshop.answertHtml, bookshop.price);

今回勉強になったこと

関数を使うのが元々苦手だったのですが引数に関数を入れたり高階関数を扱ってみたりして関数の使い方が少しは分かった気がします。
呼び出しの関数に長いコールバック関数を書いたのは可読性的に微妙な気もするんですがどうなんでしょうか。。。
もっとここはこうでしょ?的なものがありましたら是非コメントお待ちしております。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Javascript】関数の引数の使い方

⑴記事の背景
学習の一環として、【Todoリスト】を作成していく中である関数の外部で中で定めた定数を外の関数でも使用したいときにどのようにすれば使えるのか教えてもらい、それを忘れない為。備忘録。

引数とは:プログラムやある特定の数値などで、関数に渡すもの

書き方:
const 関数名  = function(引数名1,引数名2){
処理(例:return 引数1 + 引数2)
};
console.log(3,5) ▶️ 8
引数名1や引数名2などは、自分で好きな名前を付けられる。
実際に関数を使うときに具体的な名前や数値などを入力すれば良い。

<Todoリスト作成時に使った内容>

 for (let i = 0; i < radioBtn.children.length; i++) { 

             if (radioBtn.children[i].checked === true) { 
                 changeTodoDisplay(radioBtn.children[i].value); 
                 console.log(radioBtn.children[i].value); 
             } 
         } 

const changeTodoDisplay = function (radioBtnState) { 
        console.log(radioBtnState); 
        if (radioBtnState === 'all') { 
             todoShow(todos); 
       }; 
         if (radioBtnState === 'working') { 
             const filterTodo = todos.filter(function (todo) { 
                 return todo.stateBtn.textContent === "作業中"; 
             }); 
             todoShow(filterTodo); 
             console.log(filterTodo); 
         }; 
         if (radioBtnState === 'complete') { 
             const filterTodo = todos.filter(function (todo) { 
                 return todo.stateBtn.textContent === "完了"; 
             }) 
             todoShow(filterTodo); 
         }; 
     } 

今回、forの中で取り出したvalueを具体的な引数(radioBtn.children[i].value)として渡して、関数の中で使っている。 ※radioBtnState=radioBtn.children[i].value

引数を使う事で、ある関数の中で定めた定数でも、外の関数の中で引数として渡して使用することができる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む