NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

注目のタグ

    TypeScriptの学習メモ

    こんにちは。入社3年目の西ヶ谷です。
    社内研修でTypeScriptを学んだので、今回は備忘録として学んだことをいくつか書いていきます。
    筆者自身も初学者だったので、初学者向けに書いていきたいと思います。

    学習メモ

    変数宣言

    まず、変数宣言時に注意することについてです。

    変数の型は変数宣言時(コンパイル時)に決まるので、その時に型の宣言をしないと勝手にany型になってしまうことに注意しましょう。

    どういうことか、以下の例を見てみましょう。

    例:string型の変数宣言

    //型注釈+初期値代入
    let value1: string = "abc";
    
    //型注釈
    let value2: string;
    
    //初期値代入
    let value3 = "abc";
    

    上記の例で宣言した変数はちゃんとstring型の変数として宣言されています。

    では、このパターンはどうでしょうか。

    let value4;
    value4 = 10;
    value4 = "abc";

    Javaの感覚でいくと、まず1行目の時点でアウトな気がしてしまいます。
    TypeScriptでは1行目のような書き方ができますが、そこで宣言された変数はany型になってしまいます。

    変数がany型になってしまうと、2,3行目のようにいろんな型の値を変数に代入することができるようになってしまいます。
    意図せずこのような変数宣言をしてしまった場合、静的型付け言語であるTypeScriptの良さを消してしまう可能性があります。
    実際に開発を行っている際に型宣言がしっかりされておらず、ソースを読んだ時に理解しにくいことがありました。
    ソースの可読性という部分でも意識したいところです。

    また、以下の動きについても注意したいです。
    TypeScriptではブロック内で変数の値を上書きして処理を書くことができてしまいます。
    以下の例を見てください。

    // 変数宣言
    let value5 = 10;
    
    // if文ブロック内
    if (true) {
      // if文の外で定義した変数の値を上書き
      let value5 = 11;
      console.log(`if文の中はvalue5=${value5}`);
    }
    
    // if文ブロック外
    console.log(`if文の外はvalue5=${value5}`);

    では、if文の中と外でそれぞれコンソール出力した結果はそれぞれどのようになるのでしょうか?
    結果は以下のようになります。

    if文の中はvalue5=11
    if文の外はvalue5=10

    つまり、ブロック内の処理では変数の値を上書きしても処理が実行できてしまうということになります。
    筆者がこの仕様を知った時、最初に思ったことは「これは事故が起きそうだな。。」でした。
    ある処理内の中だけ変数の値を上書きして処理を行うということに違和感がかなりあります。
    実際に開発する際はこのような仕様があることに注意して開発を進めていきたいと思いました。

    関数宣言

    TypeScriptでの関数宣言のパターンは3つあります。

    【1】 functionステートメント

    function sample1(a: number, b: number): number {
        return a + b;
    }

    【2】 関数リテラル

    const sample2 = function (a: number, b: number): number {
        return a + b;
    }

    【3】 arrow関数

    const sample3 = (a: number, b: number): number => {
        return a + b;
    }

    それぞれのメソッドを呼び出して結果を見てみます。

    console.log(sample1(1, 2));
    >>3
    console.log(sample2(1, 2)); 
    >>3
    console.log(sample3(1, 2)); 
    >>3

    同じ結果になりますね。つまり、どの書き方でも同じ動きをすることが分かります。
    処理の書き換えが不可であるか否かやthisキーワードが指す対象が異なるなどの違いはあるものの、
    この時はこの関数を使用するべき!といったような使い分けはあまりありません。
    しいていえば、簡潔に可読性の高いメソッドを書くことができる【3】が今後主流の書き方になるのではないかなと思います。

    関数リテラルやarrow関数の書き方がメソッド宣言であることは、正直初見ではわからない人も多いと思います。
    どの書き方のコードを見たとしても対応できるようにしておく必要があります。

    いろいろな型宣言

    【1】ユニオン型(複数の型を持つ型)
    例ではstring型とnumber型を持つ型を宣言しています。

    type Multipletypes = string | number;
    let value6: Multipletypes = 100;

    【2】型情報をもった配列
    例ではstring型とnumber型を持つ配列の型を宣言しています。

    type Tuple = [string, number];
    let value7: Tuple = ["abc", 10];

    【3】プロパティを持った型
    例ではstring型のプロパティとnumber型のプロパティを持つ型を宣言しています。
    これは実際の開発でもよく使用した宣言方法です。
    一見するとinterfaceで宣言した場合と同じように思えるかもしれませんが、継承や同名での宣言をした際の動きが異なってきます。
    例えば、interfaceは継承が可能ですが【3】の宣言方法では継承をすることができません。
    また、interfaceは同名の宣言を行うことができますが【3】の宣言方法では同名の宣言をすることができません。

    個人的には、動きの違い以外にも以下のような考えで使い分けをしてみるのも良いと思います。
    ①プロパティの型がプリミティブ型(stringやnumberなどの基本の型)のみの場合:typeで宣言する
    ⇒既存の型を組み合わせて型を再定義しているイメージになる
    ②プロパティの型がオブジェクト型(自分で定義した型)の場合:interfaceで宣言する
    ⇒新たに型を定義したというイメージになる

    いろいろな考え方があると思いますので、こんな考え方もあるんだ!という風に思っていただければと思います。

    type Props = {
      field1: string;
      field2: number;
    };
    let value8: Props = { field1: "abc", field2: 100 };

    【4】文字リテラル型(具体的な値そのもの)のユニオン型
    例では季節を文字列で持つ型を宣言しています。
    入る文字列が決まっている時はstring型の変数を宣言するよりも、
    このように宣言するとどんな文字列が入るか明確になるのでソースの可読性が上がります。
    また、想定した文字列以外が入ってくることがないのでエラーが起こりにくいというメリットもあります。

    type Season = "spring" | "summer" | "autumn" | "winter";
    let value9: Season = "summer";

    【おまけ】文字列以外でも使うことができる
    こちらはおまけですが、このような組み合わせで変数宣言をすることもできます。

    type Numbers = 1 | 2 | string;
    let value10: Numbers = 1;
    

    おわりに

    現在はangularでフロントエンドの開発を行っています。
    angularの理解をすることにもかなり苦労しましたが、
    実際に開発していた時はTypeScriptの基本的な仕様のところで詰まることも多かった印象です。
    基礎は大切ということを実感しました。
    最後までお読みいただきありがとうございました。