- 投稿日:2020-02-23T23:00:16+09:00
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」を用意します。
「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.jsvar 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すると・・・
はい、鬱陶しいほどダイアログが並んで出てきました!!
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ボタンを押したら全てが非表示になる」という仕様にしていますが、もちろん個別で消せるようにもできます。
今回はわかりやすく大雑把にやらせてもらいました。
- 投稿日:2020-02-23T23:00:16+09:00
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」を用意します。
「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.jsvar 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すると・・・
はい、鬱陶しいほどダイアログが並んで出てきました!!
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使う方がよりスマートに作れそうな気がしてます。
- 投稿日:2020-02-23T22:33:47+09:00
test投稿
- 投稿日:2020-02-23T18:51:05+09:00
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
- 投稿日:2020-02-23T18:41:10+09:00
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.jsvar 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.jsvar 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を使えることを知って助かりました。
わかりにくい点が多いかと思いますが、ご指摘等あればよろしくお願いします。
- 投稿日:2020-02-23T17:56:26+09:00
【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.jsimport 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.jsimport 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. 装飾
編集中...
- 投稿日:2020-02-23T17:08:15+09:00
階層構造をもつTODOリストの作り方
はじめに
以前、階層構造をもつTODOリストをWebアプリケーションとして実装したので、その知見を共有します。
実際に作成したTODOリスト(OSSとして公開)
機能一覧
- メモの新規作成
- メモの編集(編集前の内容がテキストボックスに入力されている)
- メモの削除
- メモの追加(メモAの下にメモBを紐付け)
- メモを一つ上の階層に移動
- メモの一括削除
- 新規作成や編集時のテキストボックスを、ボックス外の任意の場所をクリックすることで非表示(UX)
- MemoモードとTaskモードの切り替え
- (Taskモードのみ) メモ横のチェックボックスにチェックを入れると、非同期で打ち消し線を表示
解説
技術スタックは、Docker Compose、Java、PostgreSQLがメインです。
内部では基本的にCRUDしか行っておらず、DB側で再帰的なデータを保存しています。
Docker Compose
ports
を定義して、ホストとゲストのポートをマッピングしています。サービス名
tomcat
とpostgres
でお互い接続は可能なはずですが、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> </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パターン
- 閉包テーブル
等を使ってみるといいかもしれません。
- 投稿日:2020-02-23T16:53:36+09:00
JavaScriptでブレークポイントを張る方法【Chrome】
- 投稿日:2020-02-23T16:12:14+09:00
javascript(node.js)のクエリーストリング(qs)を解説
javascript(node.js)では、受け取ったデータは実はそのままでは使えません。クライアントからは、クエリーテキストと呼ばれる形式で送られてくるので、それをエンコードしておかないといけないのです。
それを行っているのが、qsオブジェクトの「parse」です。qs.parse(string)により、受け取ったデータ(string)をエンコードし、それぞれのパラメーターの値を整理したオブジェクトに変換してくれます。後は、このオブジェクトから必須な値を取り出して利用するだけです。
- 投稿日:2020-02-23T15:29:23+09:00
謎解きが好きすぎて未経験でも2週間で謎解きサイトが作れた話
自己紹介
こんにちは、おこめなんと言う者です。
普段はFree Templateという団体で謎解きやボードゲームをつくっています。2019年のクリスマスに謎解きサイトをつくったので、その時の経験を共有して
WEB謎制作のハードルを下げられたらと思って記事を書いています。お時間ある方は『クリスマスとイタズラ好きの悪魔ちゃん』を解いてみてくれたらうれしいです。
ちなみにWEB謎制作でプログラミングの楽しさにめざめて
今は謎解きスマホアプリを制作中です!
作りおわったらまた記事を書くと思うのでよろしくお願いします。対象の読者
web謎作ってみたいけどやり方わからない謎クラスタ
謎解きに興味あるエンジニア謎解きに興味もってくれて、「作りたい!」って思った人はTwitterでDMくれたら全力で手伝わせていただきます。
環境/言語
環境:wordpress(Cocoon)
言語:html,css,javascript,jQueryもともと謎解きのブログとして運用していたwordpressのサイト上で作っています。
Cocoonというテーマを使うと、編集画面からjavascriptを書けるので簡単にできます。基本的に画面の見た目はhtml,cssというものを使って
「クリックしたら〇〇する」みたいな条件はjavascriptを使うとできます。
jQueryはjavascriptみたいなものだと思ってます。(間違っていたらごめんなさい)
書き方が違うだけであまりやってることは変わらないと思います。具体的な例
ここからはネタバレになってしまうので、まだ解いていない人で解きたい方はこちらが終わってから読んでください。
難しいと思ったところだけピックアップして説明しています。アドベントカレンダー
の記事を参考にしました。
コピペでこんなにかっこいいカレンダーがつくれるのすごいですよね。あまり難しいことはわからないので、雪を降らすのはあきらめました。
重要なのはカレンダーの扉が開くなので!下記のコードは扉の一部です。
ざっくり説明すると
クリックしたときだけ特殊な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>クリックで画像切り替え
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が正解済みであることをチェックしている部分です。
蜘蛛の巣を動かす
こちらが参考サイトです。
・ドラッグアンドドロップする
JavaScriptを使って要素をドラッグ&ドロップで移動・枠に吸い込まれるような見た目をつくる
こちら↑の二つのサイトをコピペして組み合わせると蜘蛛の巣をドラッグアンドドロップして赤枠にはめると雪の結晶になる演出ができます。
スマホ全機種に対応するのが難しくて枠にはめる判定値(どれくらい近くにきたらはまった判定にするか)を大きくとらざるを得なかったので、クリックしただけで蜘蛛の巣が枠にはまった機種もあると思います。ごめんなさい。
あとはスマホだとドラッグしにくかったと思います。PC用にサイトをつくってしまったのが今回の反省点の一つです。
反省点
- レスポンシブ対応
- PC用の見た目を先につくってしまったので、スマホの見た目に修正するのがつらかったです。 次回つくるときは、スマホの見た目からつくることにします。
- 以前の作業を忘れる
- 1日3~4時間ずつ作っていたのですが前日やった作業を思い出すのに時間がかかっていた気がします。 まとまった時間をとって一気に作った方がいいと思いました!
- 名前を適当につけすぎた
- よくわからないフラグやクラス名が沢山あって過去の自分をぶんなぐりたくなりました
- よくわからないけど使ってみ過ぎた
- コピペを多用しすぎて、何やってるか分からないまま進めたので細かい修正が全くできなくて困りました。 少しは理解しつつ進めないとだめですね・・・
- 表示遅すぎ
- 勉強不足でいいプログラミングができていないのに加えて画像を多用しているのでサイトがめちゃくちゃ重くなってしまいました。 いらない処理を消したり、画像を軽くするともっと早くなると思います。
まとめ
プログラミング未経験でしたが、ググりまくってコピペしまくれば低クオリティだけどなんとか動かせるものは作れるということがわかりました。
謎クラの皆さんぜひWEB謎いっぱいつくって解かせてください!
- 投稿日:2020-02-23T15:19:16+09:00
Three.jsの超基本
Three.jsを触ってみたので、自身が忘れないために基本的な部分を書き残します。
超基本2はこちら->Three.jsの超基本2
Three.jsって何?
Wikipediaより引用
three.jsは、ウェブブラウザ上でリアルタイムレンダリングによる3次元コンピュータグラフィックスを描画する、クロスブラウザ対応の軽量なJavaScriptライブラリ及びアプリケーションプログラミングインタフェースである。要は、Three.jsを使えばwebブラウザ上で簡単に3DCGのコンテンツを作れるということです。
Three.jsの導入
以下のサイトからjsファイルをダウンロード。
・threejsCDNはこちらから
・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); })();
- 投稿日:2020-02-23T11:56:53+09:00
TypeScript 3.8 の発表
Announcing TypeScript 3.8 の日本語訳です。
TypeScript 3.8 は多くの新機能をもたらし、中には新しいものや、まもなく公開される ECMAScript 標準機能、型に限定した import/export のための新しいシンタックス、そしてさらに多くのものがあります。
Index
- Type-Only Imports and Exports
- ECMAScript Private Fields
- export * as ns Syntax
- Top-Level await
- JSDoc Property Modifiers
- Better Directory Watching on Linux and watchOptions
- "Fast and Loose" Incremental Checking
- Editor Features
- Breaking Changes
Type-Only Imports and Exports
この機能は、ほとんどのユーザーは考える必要はないかもしれません。しかし、もしあなたがここでの問題に直面していたら、それは興味の対象かもしれません(特に
--isolatedModules
やtranspileModule
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
を使用するためにasync
function をしばしば導入し、そしてそれを定義した後でその function をすぐさま呼んでいました。async function main() { const response = await fetch("..."); const greeting = await response.text(); console.log(greeting); } main() .catch(e => console.error(e))
async
function の導入を避けるため、近く発表される ECMAScript の使いやすい機能、"top-levelawait
"を使用することができます。以前は JavaScript において(似た機能を伴う他のほとんどの言語に沿って)、
await
はasync
function の本文中でのみ許可されていました。しかし、top-levelawait
によって、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
かそれ以上で、module
がexnext
あるいはsystem
であるときのみ、top levelawait
を使用できます。いくつかの環境とバンドラーの中でのサポートは、制限されるかもしれず、あるいは実験的サポートを有効にすることを要求するかもしれません。実装の更なる情報は、元の pull requestをご確認ください。
es2020
fortarget
andmodule
Kagami Sascha Rosylight (saschanaz)のおかげで、TypeScript 3.8 は
es2020
をmodule
やtarget
に対するオプションとしてサポートします。これは、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.json
とjsconfig.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.ts
とfileB.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`Call Hierarchy
与えられたファンクションの呼び出し元を把握するのはしばしば便利です。TypeScript は宣言の全ての参照を見つける方法をもっており(すなわちFind All Referencesコマンド)、ほとんどの人々はその質問に答えるためにそれを利用できます。しかし、それはわずかに厄介になり得ます。例えば、
foo
と名付けられたファクションの呼び出し元を探し出そうとすることを想像してください。export function foo() { // ... } // later, much farther away from 'foo'... export function bar() { foo(); } export function baz() { foo() }
foo
がbar
とbaz
によって呼び出されたのだと発見すると、同様にbar
とbaz
も呼び出し元を知りたくなります!そのようにして、bar
とbaz
に対しても同様にFind All Referencesを行使することができますが、元々答えようとしていた質問である「foo
の呼び出し元は何か」のコンテキストを失います。ここでのその制約に対処するため、幾らかのエディタはShow Call Hierarchyと呼ばれるコマンドを通じてファンクションが呼び出される経路を可視化する機能を持っており、TypeScript 3.8 は公式にCall Hierarchy機能をサポートします。
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
の直近の呼び出し元がcallerA
とcallerB
であることが見て取れます。callerA
を呼び出すのは何かを知りたい場合、callerB
と呼ばれるファンクションに沿って、プログラムのエントリーポイントがそれを直に呼び出すことを確認できます。callerB
とcallerC
の呼び出し元をさらに展開して、それらがentryPoint
ファンクション内でのみ呼ばれていることを確認できます。Call HierarchyはVisual Studio Code Insiders内の TypeScript/JavaScript に対して既にサポートされており、次の stable バージョンで利用可能になるでしょう。
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
次のコードでは、現在
param
はnoImplicitAny
のもとでエラーと共にマークされています。function foo(f: () => void) { // ... } foo((param?) => { // ... });これは、
foo
中のf
の型に対して一致するパラメータがないことが原因です。これは意図されたものではないように見えますが、param
に対して明示的な型を提供することで回避できます。
object
in JSDoc is No Longerany
UndernoImplicitAny
歴史的には、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
型がtoString
やvalueOf
のようなメソッド以外のわずかな能力と共に極端に曖昧な型である時からずっとです。しかし、TypeScript は
object
(ローワーケースのo
であることに気づいてください)と名付けられたより便利な型を持っています。object
型は、Object
よりもさらに厳しく、その中ではstring
,boolean
そしてnumber
のような全てのプリミティブ型を拒絶します。残念ながら、Object
とobject
の両方は、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
- 投稿日:2020-02-23T11:56:53+09:00
Announcing TypeScript 3.8の和訳
Announcing TypeScript 3.8 の日本語訳です。
TypeScript 3.8 は多くの新機能をもたらし、中には新しいものや、まもなく公開される ECMAScript 標準機能、型に限定した import/export のための新しいシンタックス、そしてさらに多くのものがあります。
Index
- Type-Only Imports and Exports
- ECMAScript Private Fields
- export * as ns Syntax
- Top-Level await
- JSDoc Property Modifiers
- Better Directory Watching on Linux and watchOptions
- "Fast and Loose" Incremental Checking
- Editor Features
- Breaking Changes
Type-Only Imports and Exports
この機能は、ほとんどのユーザーは考える必要はないかもしれません。しかし、もしあなたがここでの問題に直面していたら、それは興味の対象かもしれません(特に
--isolatedModules
やtranspileModule
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
を使用するためにasync
function をしばしば導入し、そしてそれを定義した後でその function をすぐさま呼んでいました。async function main() { const response = await fetch("..."); const greeting = await response.text(); console.log(greeting); } main() .catch(e => console.error(e))
async
function の導入を避けるため、近く発表される ECMAScript の使いやすい機能、"top-levelawait
"を使用することができます。以前は JavaScript において(似た機能を伴う他のほとんどの言語に沿って)、
await
はasync
function の本文中でのみ許可されていました。しかし、top-levelawait
によって、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
かそれ以上で、module
がexnext
あるいはsystem
であるときのみ、top levelawait
を使用できます。いくつかの環境とバンドラーの中でのサポートは、制限されるかもしれず、あるいは実験的サポートを有効にすることを要求するかもしれません。実装の更なる情報は、元の pull requestをご確認ください。
es2020
fortarget
andmodule
Kagami Sascha Rosylight (saschanaz)のおかげで、TypeScript 3.8 は
es2020
をmodule
やtarget
に対するオプションとしてサポートします。これは、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.json
とjsconfig.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.ts
とfileB.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`Call Hierarchy
与えられたファンクションの呼び出し元を把握するのはしばしば便利です。TypeScript は宣言の全ての参照を見つける方法をもっており(すなわちFind All Referencesコマンド)、ほとんどの人々はその質問に答えるためにそれを利用できます。しかし、それはわずかに厄介になり得ます。例えば、
foo
と名付けられたファクションの呼び出し元を探し出そうとすることを想像してください。export function foo() { // ... } // later, much farther away from 'foo'... export function bar() { foo(); } export function baz() { foo() }
foo
がbar
とbaz
によって呼び出されたのだと発見すると、同様にbar
とbaz
も呼び出し元を知りたくなります!そのようにして、bar
とbaz
に対しても同様にFind All Referencesを行使することができますが、元々答えようとしていた質問である「foo
の呼び出し元は何か」のコンテキストを失います。ここでのその制約に対処するため、幾らかのエディタはShow Call Hierarchyと呼ばれるコマンドを通じてファンクションが呼び出される経路を可視化する機能を持っており、TypeScript 3.8 は公式にCall Hierarchy機能をサポートします。
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
の直近の呼び出し元がcallerA
とcallerB
であることが見て取れます。callerA
を呼び出すのは何かを知りたい場合、callerB
と呼ばれるファンクションに沿って、プログラムのエントリーポイントがそれを直に呼び出すことを確認できます。callerB
とcallerC
の呼び出し元をさらに展開して、それらがentryPoint
ファンクション内でのみ呼ばれていることを確認できます。Call HierarchyはVisual Studio Code Insiders内の TypeScript/JavaScript に対して既にサポートされており、次の stable バージョンで利用可能になるでしょう。
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
次のコードでは、現在
param
はnoImplicitAny
のもとでエラーと共にマークされています。function foo(f: () => void) { // ... } foo((param?) => { // ... });これは、
foo
中のf
の型に対して一致するパラメータがないことが原因です。これは意図されたものではないように見えますが、param
に対して明示的な型を提供することで回避できます。
object
in JSDoc is No Longerany
UndernoImplicitAny
歴史的には、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
型がtoString
やvalueOf
のようなメソッド以外のわずかな能力と共に極端に曖昧な型である時からずっとです。しかし、TypeScript は
object
(ローワーケースのo
であることに気づいてください)と名付けられたより便利な型を持っています。object
型は、Object
よりもさらに厳しく、その中ではstring
,boolean
そしてnumber
のような全てのプリミティブ型を拒絶します。残念ながら、Object
とobject
の両方は、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
- 投稿日:2020-02-23T11:56:53+09:00
[和訳]Announcing TypeScript 3.8
Announcing TypeScript 3.8 の日本語訳です。
TypeScript 3.8 は多くの新機能をもたらし、中には新しいものや、まもなく公開される ECMAScript 標準機能、型に限定した import/export のための新しいシンタックス、そしてさらに多くのものがあります。
Index
- Type-Only Imports and Exports
- ECMAScript Private Fields
- export * as ns Syntax
- Top-Level await
- JSDoc Property Modifiers
- Better Directory Watching on Linux and watchOptions
- "Fast and Loose" Incremental Checking
- Editor Features
- Breaking Changes
Type-Only Imports and Exports
この機能は、ほとんどのユーザーは考える必要はないかもしれません。しかし、もしあなたがここでの問題に直面していたら、それは興味の対象かもしれません(特に
--isolatedModules
やtranspileModule
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
を使用するためにasync
function をしばしば導入し、そしてそれを定義した後でその function をすぐさま呼んでいました。async function main() { const response = await fetch("..."); const greeting = await response.text(); console.log(greeting); } main() .catch(e => console.error(e))
async
function の導入を避けるため、近く発表される ECMAScript の使いやすい機能、"top-levelawait
"を使用することができます。以前は JavaScript において(似た機能を伴う他のほとんどの言語に沿って)、
await
はasync
function の本文中でのみ許可されていました。しかし、top-levelawait
によって、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
かそれ以上で、module
がexnext
あるいはsystem
であるときのみ、top levelawait
を使用できます。いくつかの環境とバンドラーの中でのサポートは、制限されるかもしれず、あるいは実験的サポートを有効にすることを要求するかもしれません。実装の更なる情報は、元の pull requestをご確認ください。
es2020
fortarget
andmodule
Kagami Sascha Rosylight (saschanaz)のおかげで、TypeScript 3.8 は
es2020
をmodule
やtarget
に対するオプションとしてサポートします。これは、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.json
とjsconfig.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.ts
とfileB.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`Call Hierarchy
与えられたファンクションの呼び出し元を把握するのはしばしば便利です。TypeScript は宣言の全ての参照を見つける方法をもっており(すなわちFind All Referencesコマンド)、ほとんどの人々はその質問に答えるためにそれを利用できます。しかし、それはわずかに厄介になり得ます。例えば、
foo
と名付けられたファクションの呼び出し元を探し出そうとすることを想像してください。export function foo() { // ... } // later, much farther away from 'foo'... export function bar() { foo(); } export function baz() { foo() }
foo
がbar
とbaz
によって呼び出されたのだと発見すると、同様にbar
とbaz
も呼び出し元を知りたくなります!そのようにして、bar
とbaz
に対しても同様にFind All Referencesを行使することができますが、元々答えようとしていた質問である「foo
の呼び出し元は何か」のコンテキストを失います。ここでのその制約に対処するため、幾らかのエディタはShow Call Hierarchyと呼ばれるコマンドを通じてファンクションが呼び出される経路を可視化する機能を持っており、TypeScript 3.8 は公式にCall Hierarchy機能をサポートします。
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
の直近の呼び出し元がcallerA
とcallerB
であることが見て取れます。callerA
を呼び出すのは何かを知りたい場合、callerB
と呼ばれるファンクションに沿って、プログラムのエントリーポイントがそれを直に呼び出すことを確認できます。callerB
とcallerC
の呼び出し元をさらに展開して、それらがentryPoint
ファンクション内でのみ呼ばれていることを確認できます。Call HierarchyはVisual Studio Code Insiders内の TypeScript/JavaScript に対して既にサポートされており、次の stable バージョンで利用可能になるでしょう。
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
次のコードでは、現在
param
はnoImplicitAny
のもとでエラーと共にマークされています。function foo(f: () => void) { // ... } foo((param?) => { // ... });これは、
foo
中のf
の型に対して一致するパラメータがないことが原因です。これは意図されたものではないように見えますが、param
に対して明示的な型を提供することで回避できます。
object
in JSDoc is No Longerany
UndernoImplicitAny
歴史的には、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
型がtoString
やvalueOf
のようなメソッド以外のわずかな能力と共に極端に曖昧な型である時からずっとです。しかし、TypeScript は
object
(ローワーケースのo
であることに気づいてください)と名付けられたより便利な型を持っています。object
型は、Object
よりもさらに厳しく、その中ではstring
,boolean
そしてnumber
のような全てのプリミティブ型を拒絶します。残念ながら、Object
とobject
の両方は、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
- 投稿日:2020-02-23T11:56:53+09:00
TypeScript 3.8 の発表記事を和訳してみた
TypeScript 3.8 で発表された機能を理解するため、発表記事を和訳しました。原文はこちら
TypeScript 3.8 は多くの新機能をもたらし、中には新しいものや、まもなく公開される ECMAScript 標準機能、型に限定した import/export のための新しいシンタックス、そしてさらに多くのものがあります。
Index
- Type-Only Imports and Exports
- ECMAScript Private Fields
- export * as ns Syntax
- Top-Level await
- JSDoc Property Modifiers
- Better Directory Watching on Linux and watchOptions
- "Fast and Loose" Incremental Checking
- Editor Features
- Breaking Changes
Type-Only Imports and Exports
この機能は、ほとんどのユーザーは考える必要はないかもしれません。しかし、もしあなたがここでの問題に直面していたら、それは興味の対象かもしれません(特に
--isolatedModules
やtranspileModule
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
を使用するためにasync
function をしばしば導入し、そしてそれを定義した後でその function をすぐさま呼んでいました。async function main() { const response = await fetch("..."); const greeting = await response.text(); console.log(greeting); } main() .catch(e => console.error(e))
async
function の導入を避けるため、近く発表される ECMAScript の使いやすい機能、"top-levelawait
"を使用することができます。以前は JavaScript において(似た機能を伴う他のほとんどの言語に沿って)、
await
はasync
function の本文中でのみ許可されていました。しかし、top-levelawait
によって、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
かそれ以上で、module
がexnext
あるいはsystem
であるときのみ、top levelawait
を使用できます。いくつかの環境とバンドラーの中でのサポートは、制限されるかもしれず、あるいは実験的サポートを有効にすることを要求するかもしれません。実装の更なる情報は、元の pull requestをご確認ください。
es2020
fortarget
andmodule
Kagami Sascha Rosylight (saschanaz)のおかげで、TypeScript 3.8 は
es2020
をmodule
やtarget
に対するオプションとしてサポートします。これは、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.json
とjsconfig.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.ts
とfileB.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`Call Hierarchy
与えられたファンクションの呼び出し元を把握するのはしばしば便利です。TypeScript は宣言の全ての参照を見つける方法をもっており(すなわちFind All Referencesコマンド)、ほとんどの人々はその質問に答えるためにそれを利用できます。しかし、それはわずかに厄介になり得ます。例えば、
foo
と名付けられたファクションの呼び出し元を探し出そうとすることを想像してください。export function foo() { // ... } // later, much farther away from 'foo'... export function bar() { foo(); } export function baz() { foo() }
foo
がbar
とbaz
によって呼び出されたのだと発見すると、同様にbar
とbaz
も呼び出し元を知りたくなります!そのようにして、bar
とbaz
に対しても同様にFind All Referencesを行使することができますが、元々答えようとしていた質問である「foo
の呼び出し元は何か」のコンテキストを失います。ここでのその制約に対処するため、幾らかのエディタはShow Call Hierarchyと呼ばれるコマンドを通じてファンクションが呼び出される経路を可視化する機能を持っており、TypeScript 3.8 は公式にCall Hierarchy機能をサポートします。
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
の直近の呼び出し元がcallerA
とcallerB
であることが見て取れます。callerA
を呼び出すのは何かを知りたい場合、callerB
と呼ばれるファンクションに沿って、プログラムのエントリーポイントがそれを直に呼び出すことを確認できます。callerB
とcallerC
の呼び出し元をさらに展開して、それらがentryPoint
ファンクション内でのみ呼ばれていることを確認できます。Call HierarchyはVisual Studio Code Insiders内の TypeScript/JavaScript に対して既にサポートされており、次の stable バージョンで利用可能になるでしょう。
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
次のコードでは、現在
param
はnoImplicitAny
のもとでエラーと共にマークされています。function foo(f: () => void) { // ... } foo((param?) => { // ... });これは、
foo
中のf
の型に対して一致するパラメータがないことが原因です。これは意図されたものではないように見えますが、param
に対して明示的な型を提供することで回避できます。
object
in JSDoc is No Longerany
UndernoImplicitAny
歴史的には、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
型がtoString
やvalueOf
のようなメソッド以外のわずかな能力と共に極端に曖昧な型である時からずっとです。しかし、TypeScript は
object
(ローワーケースのo
であることに気づいてください)と名付けられたより便利な型を持っています。object
型は、Object
よりもさらに厳しく、その中ではstring
,boolean
そしてnumber
のような全てのプリミティブ型を拒絶します。残念ながら、Object
とobject
の両方は、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
- 投稿日:2020-02-23T11:50:37+09:00
「何か食べたいけど何が食べたいのか分からない」あなたへ
きょう何食べようか悩んでいるあなたへ
5つの質問に答えるだけで、料理ジャンルをサジェストしてくれるサービスを公開しました
https://kyounanitaberu.appspot.com/
きっかけ
きっかけは一つの動画。
私は美味しいものを食べることが大好きなんですが、好きが故に何を食べるかめちゃくちゃ悩んで時間を消費してしまいます。
この動画を見て「同じことを思っている人がいるんだ」と気づけたので
2択で今の気分を選ぶと、料理ジャンルをサジェストしてくれるWebアプリケーションを作りました。システム
システムとしてはVue.jsで作ったアプリをGCP(GoogleCloudPlatform)にデプロイしているちょーシンプルな構成です。
画面構成も”NavBarコンポーネント”と”Cardコンポーネント”と至って簡単。
$ 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を勉強したての人が一通り復習するのにいいかも、と思いました
また、GCPへのデプロイは、Vue.jsで作成したSPAなアプリをGoogle App Engineへデプロイするを参考にさせていただきました、ありがとうございます。
感想
昨日さっそく使ってみたのですが、いつも30分くらい悩むところを(悩みすぎ)5分くらいでパパッと決められて満足です。
改善点は色々ありますが、質問や回答を自分で考えてファイルとして定義しているので、こういうところで機械学習を使っていきたいなーと思いました。
最後に、私はVueもGCPも初心者ですが1〜2日(ずっと開発してたわけではない)でゼロから公開までできました。
VueやGCPに興味をお持ちの方に「このくらいのものは作れるんだ」と思ってもらえると嬉しいです
- 投稿日:2020-02-23T09:46:26+09:00
花粉症対策デジタル医療相談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が入ります。
作成方法
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); }完成図
考察
最初はユーザーから送られるすべてのIDと位置情報をFirebaseに登録しif文で取得していたため、ユーザーが増えると処理に時間がかかりそうでした。idは重複がないように、位置情報は最新のものだけを登録できたのでスッキリして気持ちがよいですね。今後は内服薬やアレルギーの重症度、花粉飛散の実測値を登録・分析することによってユーザーの住んでいる場所の予測飛散量とユーザーの重症度から適切な治療薬を推奨できるようにしていきたいと思っています。
- 投稿日:2020-02-23T08:58:19+09:00
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);今回勉強になったこと
関数を使うのが元々苦手だったのですが引数に関数を入れたり高階関数を扱ってみたりして関数の使い方が少しは分かった気がします。
呼び出しの関数に長いコールバック関数を書いたのは可読性的に微妙な気もするんですがどうなんでしょうか。。。
もっとここはこうでしょ?的なものがありましたら是非コメントお待ちしております。
- 投稿日:2020-02-23T00:07:03+09:00
【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
引数を使う事で、ある関数の中で定めた定数でも、外の関数の中で引数として渡して使用することができる。