2021/07/04

[js]import * は普通

前回、namespace にして export default して import させていた。
namespace は無しにして import * でも同じことはできるのだが、わざわざ namespace にしてみたのだ。

その理由は「ワイルドカードよりも特定の namespace を使った方がよいのではないか」という気持ちがあったからだ。ほら、不特定なものよりも特定した方が好まれるじゃないか。

しかし。。。

TypeScript: Documentation - Namespaces and Modules
https://www.typescriptlang.org/docs/handbook/namespaces-and-modules.html

太字で

we recommended modules over namespaces in modern code.

最新のコードでは、名前空間よりもモジュールを推奨しました。
(Google翻訳)

と書いてある。
「最新のコードでは」だから将来変わるのかもしれんが、2021/07/04 19:23 ではそうなっていた。

 

なので、ワイルドカードで指定しているとかそういうのはあんまあり考えなくて良いんだろうね。変に気を回して時間を掛けてしまったよ。

[js]ファイルを分けたい

大きいプログラムを作ると、ファイルを分けたくなるだろう。そうじゃなくても、機能ごとにファイルを分けたいだろうし。
まあ、私が分けたいのだ。


index.ts

01: import * as greet from './greet';
02: 
03: const hello: greet.Greet = greet.hello();
04: if (hello != null) {
05:     console.log(`hello: ${JSON.stringify(hello)}`);
06: }

greet/index.ts

01: export type Greet = {
02:     greet: string;
03:     time?: string;
04: };
05: 
06: export function hello(): Greet {
07:     return {greet: 'hello', time: 'allday'};
08: }
09: 
10: export default {}
  • greet ディレクトリにファイルがあるので、 import の from はそこを指定する。
    • ファイル名が index.ts なのでディレクトリの指定だけで済んでいる。
  • greet/index.ts は export default {} としたので、ファイルの中にある export 指定したものは同じ名前で export される。

という理解で正しいだろうか?

 

import の from より前の部分があまりわかってない。
こうか?

  • 個別に import するなら、 import {個別にコンマ区切り} from '~~'
  • 全部 import するなら、 import 名前 from '~~'

 

greet/index.ts から個別に importすると、こうなる。

01: import {Greet,hello} from './greet';
02: 
03: const msg: Greet = hello();
04: if (msg!= null) {
05:     console.log(`hello: ${JSON.stringify(msg)}`);
06: }

列挙するのが面倒ではあるが、使用しているものがはっきりするので場合によってはわかりやすい。


じゃあ全部 import するので import Hello from './greet' でよいかというと、これはダメそうだ。
Hello.Greet や Hello.hello という指定ができなかったのだ。
「Cannot find namespace 'Hello'」というエラーになる。

namespace を使う場合はこういう書き方になるようだ。
import するときに「import Hello from './greet'」としたが、別に namespace名と同じにする必要はなさそうだった。

あと、namespace も export を指定しないと個別に import できない(個別の話。export default指定しているなら関係ない)。namespace を export しても、namespace の中身が全部 export されたことになるわけではなく、その中でも export を付けたものだけが外部から見えるようだ。

index.ts

01: import Hello from './greet';
02: 
03: const msg: Hello.Greet = Hello.hello();
04: if (msg != null) {
05:     console.log(`hello: ${JSON.stringify(msg)}`);
06: }

greet/index.ts

01: namespace Hello {
02:     export type Greet = {
03:         greet: string;
04:         time?: string;
05:     };
06: 
07:     export function hello(): Greet {
08:         return {greet: 'hello', time: 'allday'};
09:     }
10: }
11: export default Hello;

 

この greet/index.ts を import * した場合は namespace 名を使わなくても済むようだ。

index.ts

01: import * as Pochi from './greet';
02: 
03: const msg: Pochi.default.Greet = Pochi.default.hello();
04: if (msg != null) {
05:     console.log(`hello: ${JSON.stringify(msg)}`);
06: }

 

 

namespace を付けるなら、 export default ではなく個別にした方がよいのかな?と思ったが、そうすると import側でも個別にしないといけなかった。
1ファイルで複数の namespace を入れ込むことは(私の実装だと)やらないので、これは default のままの方が使い勝手がよいか。

index.ts

01: import {Hello} from './greet';
02: 
03: const msg: Hello.Greet = Hello.hello();
04: if (msg != null) {
05:     console.log(`hello: ${JSON.stringify(msg)}`);
06: }

greet/index.ts

01: namespace Hello {
02:     export type Greet = {
03:         greet: string;
04:         time?: string;
05:     };
06: 
07:     export function hello(): Greet {
08:         return {greet: 'hello', time: 'allday'};
09:     }
10: }
11: export {Hello};

 


import の後ろは、 * as xx だったり name だったり {name} だったりといろいろあるが、いったいなんなんだ。

ECMAScript 2015 Language Specification – ECMA-262 6th Edition
https://262.ecma-international.org/6.0/#sec-imports

たくさんあるので from を使うパターンだけ見ていこう。
以下、イタリック&下線の部分はさらに展開されるという意味である。

import ImportClause FromClause;

FromClause の方は種類がないので、先にそちらを。

from StringLiteral

順番に展開すると ModuleSpecifier → StringLiteral なのだが、まあよかろう。
つまり文字列だ。

ImportClause は5種類に分かれる。

  • ImportedDefaultBinding
  • * as BindingIdentifier
  • { ImportsList }
  • ImportedDefaultBinding , * as BindingIdentifier
  • ImportedDefaultBinding , { ImportsList }

たぶん BindingIdentifier は「勝手に割り当てて良い名称」だと思う。bind だから紐付ける名称になるのかな。

上3つが import でよく見る形式で、残り2つはその組み合わせだ。
使えるものを展開してるのはこちら。

import - JavaScript | MDN
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/import

まあ、こっちの方がわかりやすいな。日本語だし。


これで基本的なところはできたんじゃなかろうか。

もう1つやりたいのが、export するファイルとそうじゃないファイルを分ける作業だ。
実装を見られて困るとかではなく、C言語で言うところのヘッダファイルとソースファイルに分けたいというだけである。

できるのだろうか?
なお、tsconfig.js は isolatedModules:true を指定しているものとする。

 

re-export というのでやってみた。

 

greet/index.d.ts

01: export type Greet = {
02:     greet: string;
03:     time?: string;
04: };

greet/hello.ts

01: import {Greet} from './index.d';
02: 
03: export function hello(): Greet {
04:     return {greet: 'hello', time: 'allday'};
05: }

 

greet/index.ts

01: export {hello} from './hello';
02: export type {Greet} from './index.d';

 

index.ts

01: import {hello,Greet} from './greet';
02: 
03: const msg: Greet = hello();
04: if (msg != null) {
05:     console.log(`hello: ${JSON.stringify(msg)}`);
06: }

うーん、無理やりやりましたという感じが拭えない。
それに呼び出し元の index.ts の方に修正が入るのはダメだ。

 

これは書き方としてダメだった。namespace 内ではダメだそうだ。

01: export namespace Hello {
02:     export {hello} from './hello';
03:     export type {Greet} from './index.d';
04: };
05: export default Hello;

 

これならいけるようだ。

01: import {hello as myHello} from './hello';
02: import {Greet as myGreet} from './index.d';
03: 
04: export namespace Hello {
05:     export const hello = myHello;
06:     export type Greet = myGreet;
07: };
08: export default Hello;

うーん・・・。もっとよいやり方がありそうな気がする。

 

というわけで、あきらめて本を買うことにした。

O'Reilly Japan - プログラミングTypeScript
https://www.oreilly.co.jp/books/9784873119045/

本の中身は書かないのだが、私が import あたりをうまく理解できない理由が分かってきた気がする。

「JavaScript」とか「TypeScript」とかで検索しながらやっていたけど、import というかモジュールの扱いについて過去の経緯がいろいろあり、ネットで見つけた情報はどの時点の JavaScript について書いているかによって変わってくるからだと思う。そういうのを知らずにネットで調べていたので、あっちの情報とこっちの情報をまぜこぜにしてうまくいかなかったとか、そんな感じになったんじゃなかろうか。

本の内容で勉強することの利点は、よくも悪くも情報が混ざらないことだと思うので、あやふやな知識でふらふら調べるよりはましなんじゃなかろうか。せっかくお金出したんだから、ましであってほしい。

2021/07/03

[js]nullチェックとtruthy

JavaScript で null チェックする際、私はこう書くことが多い。

let val;

...

if (val) {
  // not null
  ...
} else {
  // null
  ...
}

C言語だと 0 以外の値なら真なのでアドレス値が入っているかどうかを判定するし、 Java だとそういう書き方は興られたような気がする。

で、JavaScript の場合はどういう仕様なのか?

 

Truthy - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN
https://developer.mozilla.org/ja/docs/Glossary/Truthy

falsy として定義された値以外は truthy 扱いで、 truthy は boolean 判定するところに出てきたら true として扱うという仕様だった。

falsy はこれら。

  • false
  • 0
  • -0
  • 0n
  • ''(空文字列)
  • null
  • undefined
  • NaN

オブジェクトの場合は null か undefined かそれ以外になるはずなので、 if (オブジェクトの変数) だけで事足りるはずだ。


あとは、その書き方が一般的なのかどうか、だ。
私は if (変数) や if (!変数) の書き方で見慣れているのだが、一般的にそうでないなら使いどころは絞った方がよいと思うのだ。

nullとundefined - TypeScript Deep Dive 日本語版
https://typescript-jp.gitbook.io/deep-dive/recap/null-undefined

うーん「== null」って書いてあるな。

確かに、 != null だと null か undefined だけだが、変数名だけだと幅が広すぎるのか。
x && x.length > 0 とか x && x !== 0 だったら変数名だけでよいかもしれないが、やっぱりちゃんと書いた方が無難だろう。

 

というわけで、 null / undefined のチェックにはきちんと == null のように条件を書くことにしよう。


ついでに読んでいてわからなかったのがこちら。

ルートレベル(root level)のundefinedのチェック
https://typescript-jp.gitbook.io/deep-dive/recap/null-undefined#rtoreberuroot-levelnoundefinednochekku

null だろうと undefined だろうと、 x.y を参照するのに x が どこも指していないのだったらダメだろう。

undefined かどうかをチェックするには確かに typeof するしかないのだが、 null は別によいってことなんだろうか。

うーん、わからん。。。

 

ごにょごにょ考えたが、あれは x.y != null みたいなチェックをいきなりするのではなく、先に x をチェックしろということを言いたいだけなんだろうか。
それならわかるけど、やっぱり x != null だけでよいと思うのだ。

Checking for root level undefined
https://basarat.gitbook.io/typescript/recap/null-undefined#checking-for-root-level-undefined

原文も同じ。

まあいいや。
ルートレベルより下のプロパティやメソッドにアクセスする前にはルートレベルのチェックをするか try-catch を考慮するようにしておけば良かろう。

TypeScriptとnull/undefinedチェック

JavaScript のことを書いているサイトであればほぼ書かれていると思われる null/undefined チェック問題。問題というよりは FAQ なんだろうけど、私は何度やっても覚えられない。


まず、 null と undefined の型だが、これはオブジェクトではなくプリミティブ型とのこと。昔ちょっと JavaScript を書いたときは 'undefined' と文字列になっていたような気がするが、そう書いてある()からそうなんだろう。

にもかかわらず、 typeof null は "object" だ。 typeof undefined は "undefined" である。
nullは「プリミティブ値」と表現されていたし、undefined も同様だ

Primitive (プリミティブ) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN
https://developer.mozilla.org/ja/docs/Glossary/Primitive

undefinedはプリミティブデータ型なので typeof で "undefined" になるが、 null は「プリミティブに見える」と微妙な表現をされている特殊なオブジェクトだそうな。あらゆるオブジェクトは nullから派生するらしい。

 

等値演算子(==, !=)を使う場合、null と undefined は同類と見なされる。

01: let val = null;
02: 
03: if (val) {
04:   console.log('defined');
05: } else {
06:   console.log('undefined');
07: }

これはどっちを通ったかというと、"undefined" の方を通った。 1行目が undefined でも同じだ。

null と undefined を区別したいなら同値演算子(===, !==)を使う。

A user name of null?
https://developer.mozilla.org/ja/docs/Learn/Getting_started_with_the_web/JavaScript_basics#a_user_name_of_null

この例は (!myName || myName === null) となっているのだが、 !myName だけでよいのでは?
もし && だったら myName === null だけでよいだろうし。

 

  • undefined はプリミティブデータ型でありプリミティブ値である
  • null はプリミティブ値のようでありオブジェクトのようであり、特殊な扱い
  • undefined と null を同じ扱いにして良いなら等値演算子(==, !=)で扱える
    • x == null とか x == undefined とか
    • !x でもよい
  • undefined かどうかを調べたいなら typeof === 'undefined' にする
    • null の typeof は 'object' になる

 

ちなみに、変数に undefined を与えて console.log したのが上、 'undefined' を与えて console.log したのが下。

image

null だとどっちで与えても白い文字で出力された。 うーん。

console.log なのか JavaScript 全体なのか分からないけど、人間寄りになるように動いてしまうのが苦手意識を持ってしまう原因かもしれん(私がね)。


TypeScript は型定義があるため、もうちょっと複雑になりそうだ。

nullとundefined - TypeScript Deep Dive 日本語版
https://typescript-jp.gitbook.io/deep-dive/recap/null-undefined

等値演算子で null チェックすればよいよ、ということだ。

といっても、ここは TypeScript の仕様説明ではないので、もうちょっと確認しよう。

 

まず、 null が型の方として扱われるようだ。

let val: number = null;

これはコンパイルエラーだ。どうしても代入したければ、私は null も OKですよ、ということにするしかないのか。

let val: number | null = null;

ちなみに、これもダメだ。

let val: number = null as number;

 

null は型扱いなので、変数を null型で指定できるようだ。

let val: null = null;
val = 'hello';

こんなのは代入する行でコンパイルエラーになる。 undefined もそうだった。

let val: undefined;
val = null;

使い道はないけどね。

 

だから、 null や undefined を代入したいのなら型のところで | null のような形にすることになる。
なるのだが、 undefined はデフォルト値になるので、ちょっと特殊な扱いかもしれない。その代わり、代入しないまま使おうとしたら、 | undefined になっていない限りはエラーになる。型と異なる変数が入っている、という扱いかもしれないが、TS2454 コンパイルエラーは before being assigned なので別エラーにはなっている。


C言語をよく使う人からすると、使い終わった変数に null を代入したいという気持ちに駆られることがある。

開いたファイルを扱うオブジェクトの変数があって、その変数が null だったら先にオープンしてから処理を続ける、みたいな。それをやろうとすると、最初は undefined だからよいとして、一度オープンして、クローズしたら null にしたくなる。しかし null を代入させるなら | null で許容させる必要がある。

私の期分の問題かもしれないが、 | null というか、型を複数割り当てるというのがどうも苦手だ。 string | number とかされたら「どっちが入ってんだよ!」ってなるからやらないけど、似たような気持ちになってしまう。

なら指定しなければ良いのだが、そうじゃなかったら boolean の変数を用意して判断するみたいなことになるだろう。でも、そこまでするのはなんか大げさな気がしてしまう。

なので、あまり選びたくない2択のどちらかをせねばならぬ。
あるいは、そういう設計の仕方自体が JavaScript らしくないということかもしれない。

 

そういう、言語仕様ではなく、センスというかお作法というかがよくわかっていないというのが最大の悩みであった。

[js]Arrayと配列と型付き配列 (2)

こんな話を2回に分ける必要もないのだが、日記だから良いのだ。

Arrayクラスと型付き配列は別物っぽいという話だ。

JavaScript 型付き配列 - JavaScript | MDN
https://developer.mozilla.org/ja/docs/Web/JavaScript/Typed_arrays

型付き配列というのは、いわゆる C言語などでの配列に近いもののようである。ただ、C言語では単に型のデータを配列にするのだが、型付き配列は「xxx型の配列のクラス」であり、その型ごとにクラスが用意されているところが異なる。

なので、定義されたクラス以外は(デフォルトでは)存在しない。

  • Int8Array
  • Uint8Array
  • Uint8ClampedArray
  • Int16Array
  • Uint16Array
  • Int32Array
  • Uint32Array
  • Float32Array
  • Float64Array
  • BigInt64Array
  • BitUint64Array

まあ、こんだけあれば十分だね。

どうやら、JavaScript では最初に Arrayクラスがあって、その後で型付き配列ができたようだ。データがメモリとして連続しているという前提がないと高速化できないというのはよくわかる。

つまり、Arrayクラスとは別物であるし、成り立ちからしても Arrayクラスの派生ではないだろう。 ArrayBufferクラスというものがあるようなので、むしろそちらが基底クラスになっているだろうか? しかし分類としては Array側になっていた。まあ配列だしね。

索引付きコレクション
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects#indexed_collections

なんかこう、Java のドキュメントのように継承関係の図はないんだろうか。