16
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

型を書かないTypeScript

Last updated at Posted at 2022-03-02

TL;DR

$\tiny{(TL;DRって書く人きらい)}$

本日の結論...

$\huge{型は書くな}$

...

$\tiny{もう少し正確に}$

...

$\huge{型は自分から書くな}$

前置き

あくまで筆者の現時点(2022/3/3)での知識に基づく内容です
TypeScriptをしっかりと触り始めたのは2021/11~とかです。
間違っている内容多々あるかもです。

社内勉強会用に作成した資料です
レジュメ的な立ち位置として作成した資料ですので、至る所が言葉足らずです。
ご了承ください。

書かないこと

  • 詳しい方の種類
    • プリミティブ型, ユニオン型, タプル型, void, never, unknownetc...
  • 型ガードについて
    • is, ユーザー定義型ガード

TypeScriptとは

Javascriptに型システムが導入されたヤツ。

型エラーで怒ってくれる(怒ってくれることにより実行時エラーを未然に防ぐ)

怒られたら型書けばいいだけのJavascript

型は自分から書くんじゃなくて、怒られたら書けばいいじゃない!!!

$\huge{型は自分から書くな}$

終 - NHK

...とは言っても、毎度毎度怒られるの待ちでは指示待ちマンになってしまうので、どんな時に型をつけなければいけないのか、そのケースを知っておきましょう。

型付け

TypeScriptにおいて、型を明記する必要があるのは以下の2つの場合のみです。(当社調べ)

  • 1. 後から使う・変更するもの
  • 2. TypeScriptに理解(わか)らせるもの

1. 後から使う・変更するもの

1-1. 変数
1-2. 関数
1-3. 配列・オブジェクト

1-1. 変数

後から変更するもの代表。

let test: number;

if (bool) {
    test = 100;
} else {
    test = 200;
}
console.log(test);
test = 'hoge'; // NG
// Type 'string' is not assignable to type 'number'.

初期化すればTypeScriptが推論してくれるので、型付けも不要です。

let test = 0; // number型に推論

test = 100; // OK
test = 'hoge' // NG
// Type 'string' is not assignable to type 'number'.

const hoge: string = 'hoge'; // こういうのは不要
// そもそもhogeはstring型ではなく'hoge'型となる。(リテラル型)

型付けしていなければ怒られるので、怒られてから型宣言すればいい
型推論されていれば怒られないので、怒られなければ書かなくていい

$\Large{型は自分から書くな}$

1-2. 関数

後から使うもの代表。
定義時と実行時は別だから(即時関数以外)

  • 引数の型付けが必要
  • 返り値の型付けは不要
// argにはあとで値を突っ込むから型宣言が必要
// `@returns {void}`は自動で推論
const func = (arg: string) => {
    console.log(arg);
};

func('hoge'); // OK
func(100); // NG
// Argument of type 'number' is not assignable to parameter of type 'string'.
例外
  1. 引数が特定されている関数

    引数の型宣言が不要。

    elm.addEventListener('click', (e) => {
    	// addEventListenerの第一引数には`Event`型がくるので型は決定される
      console.log(e.type); // OK
    });
    
  2. Promiseを返す関数

    返り値の型付けが必要

    // Promise<unknown> が型推論される
    const promise = () => {
      return new Promise((resolve) => {
        setTimeout(() => {
          const test = 'test';
          resolve(test);
        });
      });
    };
    
    promise().then((res) => {
      console.log(res.length); // NG
      // Object is of type 'unknown'.
    });
    
    
    // Promiseオブジェクトに格納される型の宣言が必要
    const promise = (): Promise<string> => {
        ...
    
    promise().then((res) => {
      console.log(res.length) // OK
    });
    

    引数の型を忘れていると怒られるし、返り値は書かなくても怒られない。
    Promiseの場合だけ怒ってくれる。怒られたら書けばいい

    $\Large{型は自分から(ry}$

1-3. 配列・オブジェクト

配列・オブジェクトには後から値を突っ込める。"後から変更される"系ヤツに近い。

const array = []; // any[] なんでも入っちゃう

array.push('hoge');
array.push(100);
console.log(array); // ["hoge", 100]

const array = ['hoge']; // string[]に推論される
array.push(100); // NG

// やりたいなら下のような型付けが必要
const array: (string | number)[] = ['hoge'];
array.push(100); // OK
const obj = {}; // {}型(何も入らない)

obj['property'] = 'hoge'; // error
Object.defineProperty(obj, 'property', {
  value: 'hoge',
  writable: false
});
console.log(obj.property); // error

const obj: {property: string} = {}; // configによっては初期化しないと怒られる。回避はoptionalにすること
obj['property'] = 'hoge'; // OK

$\textrm{~ 唐突な問題のお時間 ~}$

const key = ['01', '02', '03'];
const value = ['hoge', 'fuga', 'piuo'];

const obj: {
  '01': string,
  '02': string,
  '03': string
} = {
  '01': '',
  '02': '',
  '03': ''
};

// 適切に型付けせよ
key.forEach((el, idx) => {
  obj[`${el}`] = value[idx];
})
console.log(obj);

2. TypeScriptに理解らせるもの

基本的に推論で事足りるが場合によっては事足りない。
際たる例がDOM取得

みなさんよく使うquerySelectorquerySelectorAllはどちらもElement型で要素を取得してきてくれる。
割と上位の方の親クラスなので、使いたいプロパティが生えていない場合がある

.style, .href etc...

2-1. そもそも親クラスって??

基底クラスともいう。いろんな要素オブジェクトの元となっているクラス。

EventTarget
// イベントを受け取ることや、リスナーを持つことができるオブジェクト
// すべての親。グランドマザー
// `Node`, `XMLHttpRequest`, `AudioNode`, `AudioContext`...

Node
// DOM APIの基底となるクラス
// Document, Element, Attr, Text, Comment...

Element
// Documentの中にある全ての要素オブジェクトの親
// 全ての種類の要素に共通するプロパティしか生えていない
// HTMLElement, SVGElement...

HTMLElement
// html要素を表すインターフェイス。まだまだこれも親

HTML〇〇Element
// ここまできてようやく具体的な要素オブジェクトを表す型
// 全てのhtml要素それぞれの型がある(タブンネ)

インターフェースの階層を降りるほど機能が具体化していくので、メソッドやプロパティは下に行くほど生えている(継承しているから)。
中間どころのElementでは生えてないプロパティがたくさんある。

普通の?オブジェクト同様、生えてないプロパティにアクセスしようとすると怒られる。

const obj = {
  hoge: 'hoge',
  fuga: 'fuga'
};
console.log(obj.piyo); // error

const element = document.querySelector('.test'); // Element
element.style.width = '100px'; // error

const htmlElement = document.getElementById('test'); // HTMLElement
htmlElement.hash = '#hoge'; // error 

そのための型アサーション!

const htmlAnchorElement = document.getElementById('test') as HTMLAnchorElement;
htmlAnchorElement.hash = '#hoge'; // OK
// しかしこの方法はnullの可能性を無理やり消しとばしているので、好ましくない

querySelectorなら型アサーションが便利

const htmlAnchorElement = document.querySelector<HTMLAnchorElement>('.link');
htmlAnchorElement.hash = '#hoge'; // OK
// `HTMLAnchorElement | null`となりnullの可能性を引き継いでいるので実行時エラーを回避できる

NARUTO

2-2. なんでもかんでも型アサーションの必要はない

HTMLDivElementとか完全に無意味。HTMLElementで十分。
特定の型にしか生えていないプロパティを使いたいときに限り、その型にアサーションすれば良い
怒られてから型定義すればいい

$\LARGE{型は自分からk...}$

2-3. 今更nullの話

querySelectorに限らず要素取得系のメソッドが返す型はnullの可能性を含んでいる。
要素が存在しない可能性もあるからその想定でとりあえずnullも入れて返してくる。
なぜならTypeScriptは指定された要素がドキュメント上に存在するかどうかは知るよしもないから
(これは当然でTypeScriptに問題がある、とかではない)

null の可能性を含んだ要素に特定の処理を行おうとするとtsが怒ってくれるので、実行時エラーをコンパイル時に回避することができる。マジ有能

jsの場合
const elm = document.querySelector('.test');
console.log(elm.innerHTML); // TypeError
// 以降の処理が実行されないことが、実行されるまで気づけない
tsの場合
const elm = document.querySelector('.test');
console.log(elm.innerHTML); // CompileError
// 実装せずともコンパイラが叱ってくれることでエラーを回避できる
2-4. null同様、要素の型もTypeScriptの知るところではない

『アンカー要素取得しているんだから、型もHTMLAnchorElementになってくれるだろう』とはならない。
TypeScriptは取ってくる要素が何者かなんて知る由もなく、単にメソッド毎に決められた返り値を返してくる

<a href="/hoge" class="link">link</a>
const link = document.querySelector('.link');
link.href = '/fuga'; // error
2-5. ちなみに: 個人的にDOM取得は全部querySelectorquerySelectorAllでいいと思ってる

取得時に型アサーションできることがマジ便利。nullの可能性を引き継いだまま安全にアサーションできる。

  • get〇〇Element系統は全部querySelectorで取得可能
  • childrenは動的HTMLCollectionだから予期せぬ問題が生じる可能性あり(forEachも使えない)
  • childNodeはまさにNodeリストを取得するから使いづらい
  • getElementByIdの方が動作早いらしいのでidで取得するときはそっちでも
一応こういう方法も
const elm: HTMLElement | null = document.querySelector('.test');
// nullの可能性を保持したまま型アサーションができる。お好きな方で。

まとめ

  • 「怒られたら型書く」精神でオッケー
  • 型書かなきゃなのは「後から使う・変更するもの」「TypeScriptに理解らせるもの」の2つだけ
  • DOM取得はquerySelectorが最強(諸説あり)

$\huge{型は自分から書くな}$

16
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?