aknow2

自分の興味のある事を連々と(プログラミング、モバイルアプリ、プログラミング教育)

TypescriptのInterfaceにはinstanceofは使えない。

Typescriptには、JavaやC#と同じ様にinterfaceがあります。
が、JavaC#と同じ様に扱うと怪我をします。

instanceofでinterfaceは使えない。

最初に私がビックリした事です。JavaC#では'instanceof 'で型をチェックしたり、条件分岐で使えたりします。が、Typescriptでは使えません。 例えば、IActionを定義してActionというクラスで実装して見ます。それをinstaceof IActionでチェックしてみると、、、

interface IAction {
  dosomething(): void;
}

class Action implements IAction {
  dosomething() {
    console.log('hoge');
  }
}

if (Action instanceof IAction) {
  // something to do
}

VScodeを使っていれば、if文の'IAction'の箇所でインテリセンスによってエラーが出ます。

[ts] 'IAction' only refers to a type, but is being used as a value here.

訳すと、IActionは型としてしか参照出来ません、ここでは値として参照されています。。。
これはどういう事でしょうか?JavaC#脳の人にとっては混乱するエラー文です。
instanceof はタイプ、型で検査するのだから合っているのでは?値として参照とはどういう事?
コレが何故ダメなのか、すぐに分かる方法があります。 試しにIActionの例のif文の部分を削除して、Javascriptコンパイルしてみましょう。
すると↓の様になります。

var Action = /** @class */ (function () {
    function Action() {
    }
    Action.prototype.dosomething = function () {
        console.log('hoge');
    };
    return Action;
}());

なんと、Javascript変換すると'IAction'は存在しなくなっています!
これで何故interfaceinstanceofが使えないのか分かった様な気がします。
interfaceinstanceofで比較しようにも、Javascript上に存在しないので比較出来ません。 そして、型とはJavascriptに変換後と評価出来ないもの、値とはJavascriptに変換後も評価可能なものと言えます。

というわけで、どうあがいてもTypescriptのinterfaceではinstaceofを使った型チェックをする事が出来ません。

どうしてもinstanceofを使いたいという人はinterfaceではなくclassを使うしかないでしょう。classはjavascript変換後にも残りますし、Typescript上でも型としても認識されます。抽象classを上手く使えば良いかも。

abstract class BaseAction {
  abstract dosomething(): void;
}

class Action extends BaseAction {
  dosomething() {
    console.log('hoge');
  }
}

if (Action instanceof BaseAction) { // true
  // something to do
}

Typescript ブラケット記法(Object[key])でno index signatureエラーをtype safeに解決したい。

TypescriptでObjectに対して[文字列]でアクセスするブラケット記法を用いると発生するエラー。
例えば、この様に書くとエラーが出てきてコンパイルが通りません。

interface ISomeObject {
  firstKey:      string;
  secondKey:     string;
}

const obj = {
  firstKey: "a",
  secondKey: "b",
} as ISomeObject;

const key: string = 'secondKey';
const secondValue: string = obj[key];  // Element implicitly has an 'any' type because type '() => void' has no index signature.

ブラケット記法を使いたいため、keyに'secondKey'という文字列を指定しています。obj[key]でプロパティにブラケット記法でアクセスするとコンパイラは何型が返ってくるか推定出来ずにエラーとなります。
この現象に対して色々と回避方法があるので、まとめてみました。

その1・コンパイラオプションで蹴る。

↓をtsconfigに書き加えればOK。

--suppressImplicitAnyIndexErrors

でも、コレは使いたくない。  
コレを指定するとブラケット記法でプロパティにアクセスした場合にany型を許容することになります。そうなると何型の値が返ってくるか分かりません。つまり、type safeで無くなってしまう。type safeが利点であるTypescriptでanyを許容するのはいただけない。もし、keyをtypoしてしまったら実行するまでその間違いに気づけません。

その2・ interfaceを準備して[key: string]: <型>を追加する

次の解決策、ブラケット記法でアクセスした場合に返ってくる型をコンパイラに教えてあげる。
例えば、interfaceのプロパティに[key: string]: stringを追加すると、ブラケット記法でアクセスした場合、stringが返ってくるという意味になります。 こんな感じです。

interface ISomeObject {
    firstKey:      string;
    secondKey:     string;
    [key: string]: string; // <-この行を追加!
}

const obj = {
  firstKey: "a",
  secondKey: "b",
} as ISomeObject;

const key: string = 'secondKey';
const secondValue: string = obj[key]; 

だが、コレにもちょっと問題がある。
例で示したinterfaceに存在するプロパティはstring型のみ、普通は様々な型が入りますよね。
複数の型が存在する場合は複数の型を指定すれば一応、コンパイルは通ります。
例えば、string,number,booleanがinterfaceのプロパティに存在する場合、、、

interface ISomeObject {
    firstKey:      string;
    secondKey:     number;
    thirdKey: boolean
    [key: string]: string|boolean|number;  //<-or条件で型を指定 
}

const key: string = 'secondKey';
const secondValue = obj[key]; // secondValue => string|boolean|number型

コンパイルは通りますが, secondValueはnumber型ではなく、string|boolean|number型となってしまいます。
結局、ブラケット記法でアクセスすると何型が返ってくるか分かりません。その1の解決策と同じ様な状況になってしまいます。
この方法を取れば、影響範囲はinterfaceを実装しているオブジェクトのみで、型もある程度は推定出来るので、その1の方法よりかは幾分マシですが完璧とは言えません。

その3・keyofを使う。

前提を一つ壊すことになりますが、そもそも、keyをstring型にしているのが間違いだよ!という話。
const key: stringとしていた箇所をconst key: keyof ISomeObjectに変えてみましょう。
こうする事でkeyはISomeObjectのプロパティであるfirstKey,secondKey,thirdKeyのいずれかの文字列が入るstring literal typeに変身します。

interface ISomeObject {
  firstKey:      string;
  secondKey:     number;
  thirdKey:      boolean;
}

const obj = {
  firstKey: "a",
  secondKey: 2,
  thirdKey: false
} as ISomeObject;

const key: keyof ISomeObject  = 'secondKey';
const secondValue = obj[key]; // secondValueがnumber型に!

こうすれば、secondValueがちゃんとnumber型として認識してくれます!
また、keyをtypoしてもコンパイラが間違っている事を教えてくれます。
こんな感じに、、、。

const key: keyof ISomeObject  = 'scdKey';  //  Type '"sndKey"' is not assignable to type '"secondKey" | "firstKey" | "thirdKey"'.

また、ジェネリクスを使って、type safeにブラケット記法が使える関数を作るとこんな感じになります。

const accessByBracket = <S, T extends keyof S>(obj: S, key: T) => {
  return obj[key];
};

const obj = {
  firstKey: "a",
  secondKey: 2,
  thirdKey: false
} as ISomeObject;

const value = accessByBracket(obj, 'secondKey'); // value => 2

まとめ

コンパイルオプションをいじったり、interfaceにプロパティを追加する前にkeyofを上手く使って型安全な設計に出来ないか検討しましょう。

【React Native(RN)】RNのプロジェクトフォルダ内に、別のNode.jsプロジェクトを作るとRNのビルドエラーになる

どんな時に発生するか

React Nativeのプロジェクトフォルダ内に、別のNodejsプロジェクトを作った場合に発生します。
私の場合、Node.js for MobileというスマホアプリからNode.jsが実行できるモジュールを使った際に発生しました。

現象

以下のエラーログが吐かれRNのビルドに失敗する。

jest-haste-map: @providesModule naming collision: Duplicate module name
なぜ起きるのか

ReactNativeで使用しているMetroに原因がある。Metroはnpmのグローバル環境に通常はインストールされ,ソースコードの変更をウォッチしている。なので、ビルドする時にRNのプロジェクトルートにあるpackage.jsonとnode_modulesだけでなく、子フォルダにあるpackage.jsonやnode_modulesも見に行き名前の衝突が発生してビルドエラーになる。

対策

metroに特定のフォルダを無視する様に設定する。
手順 RNのプロジェクト直下にrn-cli.config.jsを作成する。
rn-cli.config.jsに以下の様に記述する。

const blacklist = require('metro').createBlacklist;
module.exports = {
    resolver: {
      blacklistRE: blacklist([
        /無視したいフォルダ名\/.*/,
      ])
    },
  };

無視したいフォルダ名を問題の起きているフォルダ名に変更すれば出来上がり、これでビルドは通るはず。

Logcatを使って実機Androidのデバッグログをウォッチする

CordovaやReactNativeなどXプラットフォームな開発をしていて シミュレータでは大丈夫だったのに実機にインストールして起動すると何も言わずにアプリが落ちた!
そんな時はlogcatを使ってログを見てみると解決する糸口が見つかったりします。
logcatを実行するのは簡単

adb logcat

が、logcatをそのまま起動すると大量のログが吐かれ、自分のアプリのログがどこにあるか分かりません。 アプリのパッケージ名を指定して以下のコマンドを実行すれば、特定のアプリのみのログが出力される様になります。

adb shell 'logcat --pid=$(pidof -s <アプリのパッケージ名>)'

ReactNativeで簡単なアプリを作ってExpoとReactNativeCLIの比較をしてみた

作ったもの

食材毎に大さじとか小さじをグラムに変換するアプリ
既に似たような物が出ているけど、家族からの要望で作ることにした。
折角なので、前から気になっていたReactNativeで作ってみる事にした。
ちなみにReactjsは使ったことはありますが、ReactNativeは初めて触りました。

大さじ?g

大さじ?g

  • tomoaki kanayama
  • Food & Drink
  • Free

play.google.com

課題を作る

ただ作るだけでは一瞬で作れそうで面白くないので、知見を深めるために課題を作った。

  • Expo とReactNative CLI両方を試す。
  • 広告を乗せる(Admob)
  • Typescriptで書く

作った感想

ネイティブモジュールを使わないのであれば、Expoが断然、楽です。今回作ったアプリもネイティブモジュールを使わないアプリなので、Expoでまず作ってみましたが環境構築から実装完了まで2日ぐらいで出来ました。React.jsと殆どのかわらない感じなので、迷うことなく作る事が出来ました。また、リリースビルドもiOS側でXcodeを使わずにビルド出来てしまうので、マジに楽で感動します。 一方、次にReactNative cliを利用した方では結局、3日近くかかりました。アプリのソースコードはExpoアプリからコピペで移したのにです。なので、もしコーディングから始めていたら実質一週間ぐらいはかかった事になると思います。

react native cliでは何にそんなにハマったのか、まず環境構築でハマる。react-native initで吐き出されたテンプレートプロジェクトをそのまま実行しても動かない。。。issueを漁りversion固有の問題だと分かり何とか動来ましたが、最初にコードを追ってバグを探していたので時間がかかりました。無駄に時間を浪費するだけなのでReactNativeで上手くいかないと思ったら、まずはGithubのissueをチェックした方がいいです。その他には、admobのプラグインもすんなり動作せず、これもgithub issueを漁り解決しました。Expoで作っている時はあまり感じなかったのですが、ReactNativeってやっぱりまだまだ不安定な要素があるのだなと痛感しました。おかげでAndroidiOSのビルド周りに詳しくなりましたが、、、。

対して、ExpoではExpo kitで提供しているAPIであれば、ほぼ問題なく動作する安心です。Expoで作っている時にはCliで作っていた時の様な解読するのが困難なエラーが出て困った事は起こらなかったです。

また、デバッグに関してもExpoの方が素早く軽快にデバッグできました。まず、Expoではnativeのビルドが無くJSをExpoアプリへバンドルするだけなのでビルドが圧倒的に早いです。CLIで作る場合はネイティブも含めビルドされるので、結構時間がかかる。また、ホットリロードも上手く行かなくなる事があり一回アプリを落として再ビルドというパターンもCLIで作っている時は良くありました。

結論

という事で在り来たりな結論ですが、expoのAPIにはカメラやセンサー類、プッシュ通知も提供しているので、APIを眺めて要件に足りているのであればExpoで作らない理由はないと思います

Create-react-native-app(CRNA)が開発中止(アーカイブ)になっている件

ふと、CRNAGithubのページを見ると、、、
f:id:aknow2:20181026172930p:plain
アーカイブされとるやんけ!
reade meを見てみると真ん中ぐらいに、、、

Note: Create React Native App has been merged with Expo CLI You can now use expo init to create your project. 
See Quick Start in the Expo documentation for instructions on getting started using Expo CLI.

CRNAはExpo CLIにマージされたよ。これからはexpo initで同じようなテンプレが生成されるよ!詳しくはここ

今からCRNAを使って大丈夫なのか?という疑問に対して、Expoのフォーラムに回答が書いてあった。

Is Create React Native App retired? - create-react-native-app (CRNA) - Expo Forums

最新のCRNAはExpo CLIのラッパー、そして、Expo CLICRNAをベースに出来ているので、とりあえずは使っても大丈夫な様子。

とはいえ、これからReactNativeに触る人はCRNAではなく、Expo CLIの'expo init'で始めまた方が良いでしょう。ReactNativeの公式ページもCRNAではなくExpo CLIを使うようにチュートリアルが変わっています。

Getting Started · React Native

さらに詳しい経緯はこちら

Use Expo CLI in the Quick Start section of the docs · Issue #23 · react-native-community/discussions-and-proposals · GitHub

CordovaでiOS版を作ってみた。(青空速読)

青空速読

青空速読

  • tomoaki kanayama
  • Education
  • Free

今までAndroid版しか作ってなかったけど、中古でiMacを安く買えたので、せっかくのクロスプラットフォーム開発環境なのだからという事でiOS版も作ってみた。

UIはAndroidから変更なし、機能的にもintent機能を除き移行出来た。ハマったところは使っているCordovaHttpというプラグインの挙動がiOSAndroidで違い 設計を少し変更する事になったが、環境構築から3日(9時間)ぐらいで移行できた、自分で書いたコードの99%はiOSAndroidで共有出来たので満足です。

ビルドやリリース周りはiOSに疎い所もあり苦戦したが、Androidよりかは簡単にビルドが出来た。ビルドはiOSのが簡単で、リリースはandroidの方が楽勝というイメージ。ビルド周りはxCodeを使えば楽々ビルドが出来てちょい感動、さすがビルドインされた環境なだけはある。だけど、iOSのリリースはappleのリジェクトに心が折れそうになる。やれ、スクショにテスト用広告が載ってるだの、使い方がよくわからないだの結構しんどい。

最近、React Nativeでも開発を始めたので近いうちにReactNativeとCordovaで比較する記事を書いてみたい。まあ、とりあえずリリースできて良かった。