TL;DR
$\tiny{(TL;DRって書く人きらい)}$
本日の結論...
$\huge{型は書くな}$
...
$\tiny{もう少し正確に}$
...
$\huge{型は自分から書くな}$
前置き
あくまで筆者の現時点(2022/3/3)での知識に基づく内容です
TypeScriptをしっかりと触り始めたのは2021/11~とかです。
間違っている内容多々あるかもです。
社内勉強会用に作成した資料です
レジュメ的な立ち位置として作成した資料ですので、至る所が言葉足らずです。
ご了承ください。
書かないこと
- 詳しい方の種類
- プリミティブ型, ユニオン型, タプル型,
void, never, unknown
etc...
- プリミティブ型, ユニオン型, タプル型,
- 型ガードについて
-
is
, ユーザー定義型ガード
-
TypeScriptとは
Javascriptに型システムが導入されたヤツ。
↓
型エラーで怒ってくれる(怒ってくれることにより実行時エラーを未然に防ぐ)
↓
怒られたら型書けばいいだけのJavascript
↓
型は自分から書くんじゃなくて、怒られたら書けばいいじゃない!!!
$\huge{型は自分から書くな}$
...とは言っても、毎度毎度怒られるの待ちでは指示待ちマンになってしまうので、どんな時に型をつけなければいけないのか、そのケースを知っておきましょう。
型付け
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'.
例外
- 引数が特定されている関数
引数の型宣言が不要。
elm.addEventListener('click', (e) => { // addEventListenerの第一引数には`Event`型がくるので型は決定される console.log(e.type); // OK });
- 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取得。
みなさんよく使うquerySelector
とquerySelectorAll
はどちらも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の可能性を引き継いでいるので実行時エラーを回避できる
2-2. なんでもかんでも型アサーションの必要はない
HTMLDivElement
とか完全に無意味。HTMLElement
で十分。
特定の型にしか生えていないプロパティを使いたいときに限り、その型にアサーションすれば良い
怒られてから型定義すればいい。
$\LARGE{型は自分からk...}$
2-3. 今更null
の話
querySelector
に限らず要素取得系のメソッドが返す型はnull
の可能性を含んでいる。
要素が存在しない可能性もあるからその想定でとりあえずnull
も入れて返してくる。
なぜならTypeScriptは指定された要素がドキュメント上に存在するかどうかは知るよしもないから
(これは当然でTypeScriptに問題がある、とかではない)
null
の可能性を含んだ要素に特定の処理を行おうとするとtsが怒ってくれるので、実行時エラーをコンパイル時に回避することができる。マジ有能
const elm = document.querySelector('.test');
console.log(elm.innerHTML); // TypeError
// 以降の処理が実行されないことが、実行されるまで気づけない
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取得は全部querySelector
とquerySelectorAll
でいいと思ってる
取得時に型アサーションできることがマジ便利。null
の可能性を引き継いだまま安全にアサーションできる。
-
get〇〇Element
系統は全部querySelector
で取得可能 -
children
は動的HTMLCollection
だから予期せぬ問題が生じる可能性あり(forEach
も使えない) -
childNode
はまさにNode
リストを取得するから使いづらい -
getElementById
の方が動作早いらしいのでidで取得するときはそっちでも
const elm: HTMLElement | null = document.querySelector('.test');
// nullの可能性を保持したまま型アサーションができる。お好きな方で。
まとめ
- 「怒られたら型書く」精神でオッケー
- 型書かなきゃなのは「後から使う・変更するもの」「TypeScriptに理解らせるもの」の2つだけ
- DOM取得は
querySelector
が最強(諸説あり)
$\huge{型は自分から書くな}$