はじめに
この記事はTypeScript初心者に向けて基本を一通りハンズオン形式で学べる教材となっています。
世の中にTypeScriptの教材は多くありますが、どれも文法ごとに文法紹介するためのコードを教えているだけで、学んだあと実際どのように自分のアプリに適応すべきなのかイメージがわきません。
これがTypeScriptの難しいところだと思っています。
このハンズオンではJavaScriptのアプリケーションをTypeScriptに移行しながら学んだことを活かしていくことで実践的に学ぶことが可能です。
ハンズオン動画と一緒に活用
こちらの記事をさらに活用できるハンズオン動画を用意していますのでご活用ください
JavaScriptの怖い振る舞い
JavaScriptはタイプセーフ機能が存在しません。
タイプセーフとは、コンパイラやランタイムがデータ型をチェックし、型が互換性のない操作に使用されることを防いでくれる機能です。
ここを厳密にチェックすることで実装者が予想もしていない操作をされることを防ぐことができ、予想外のユーザーの使い方でランタイムエラーが発生してアプリケーション全体に影響することを防いでくれます。
たとえばJavaScriptだと以下のようなことができます。
2 + "2" => "22"
2 + null => 2
undefined + 2 => NaN
本来足されることのないものも計算できてしまうのです。
これをTypeScriptは型を利用して防ぐことができのです。
なぜTypeScriptを学ぶべきなのか?
JavaScriptと比較してTypeScriptを学習すべき利点は大きく3つあります。
1. 信頼性
TypeScriptはコンパイル時にコードチェックをしてくれるため、本番環境にアプリケーションをデプロイしたらクラッシュしてしまうようなことを劇的に減らすことができます。
2. 生産性
TypeScriptを使用すると、開発者は快適にコーディングができるようなエディタ機能を使うことができます。
オートコンプリートやリファクタリング機能、即時のエラーチェックなどが開発体験(DX)を向上させてくれます
3. 雇用可能性
多くの企業においてTypeScriptは必須条件となっており、求人に明示されていなくても求められます。
少しでもTypeScriptを知っているだけで、他のジュニアエンジニアの候補者と転職活動の際に差別化をすることが可能です。
JavaScriptでのアプリを実装
今回は書籍管理アプリをテーマにJavaScriptで簡単なアプリを作ります。
$ touch index.js
const books = [
{ title: "TypeScript入門", author: "田中太郎", available: true },
{ title: "JavaScript基礎", author: "山田花子", available: false },
{ title: "Reactと実践", author: "鈴木一郎", available: true }
]
const borrowedBooks = []
let newBookId = 1
function addNewBook(book) {
books.push(book)
return book
}
function borrowedBook(title) {
const selectedBook = books.find(book => book.title === title && book.available )
selectedBook.available = false
const newBorrowedBook = { id: newBookId++, book: selectedBook, status: "borrowed" }
borrowedBooks.push(newBorrowedBook)
return selectedBook
}
function returnBook(bookId) {
const selectedBook = borrowedBooks.find(book => book.id === bookId)
selectedBook.book.available = true
selectedBook.status = "returned"
return selectedBook
}
addNewBook({ title: "Pythonで学ぶデータサイエンス", author: "伊藤花子", return: true })
addNewBook({ title: "Vue.js入門", author: "鈴木一郎", return: true })
borrowedBook("TypeScript入門")
returnBook("1")
console.log(books)
console.log(borrowedBooks)
では実行してみましょう
$ node index.js
selectedBorrowedBook.book.available = true
^
TypeError: Cannot read properties of undefined (reading 'book')
エラーがでました。これはreturnBook("1")
で文字列の1で書籍を検索していることが原因です。
[
{
id: 1,
book: { title: 'TypeScript入門', author: '田中 太郎', available: false },
status: 'borrowed'
}
]
borrowbooksはこのようにidが1になっているので、文字列1では検索ができません
しかしJavaScriptの場合このようなエラーは実際に実行してみないとわからないためバグに気づくのに遅くなってしまいます。
それでは実際にこのコードをTypeScriptに変更していきましょう
TypeScriptに変更する
新しい作業用ディレクトリを作ってから始めていきます
$ touch index.ts
コードをそのまま貼り付けていきます
const books = [
{ title: "TypeScript入門", author: "田中太郎", available: true },
{ title: "JavaScript基礎", author: "山田花子", available: false },
{ title: "Reactと実践", author: "鈴木一郎", available: true }
]
const borrowedBooks = []
let newBookId = 1
function addNewBook(book) {
books.push(book)
return book
}
function borrowedBook(title) {
const selectedBook = books.find(book => book.title === title && book.available )
selectedBook.available = false
const newBorrowedBook = { id: newBookId++, book: selectedBook, status: "borrowed" }
borrowedBooks.push(newBorrowedBook)
return selectedBook
}
function returnBook(bookId) {
const selectedBook = borrowedBooks.find(book => book.id === bookId)
selectedBook.book.available = true
selectedBook.status = "returned"
return selectedBook
}
addNewBook({ title: "Pythonで学ぶデータサイエンス", author: "伊藤花子", return: true })
addNewBook({ title: "Vue.js入門", author: "鈴木一郎", return: true })
borrowedBook("TypeScript入門")
returnBook("1")
console.log(books)
console.log(borrowedBooks)
すると以下のようにエラーがたくさん表示されます
1つ1つエラーに対処しながらTypeScriptを学んでいきましょ
nullにアクセスしないように防御する
まずはselectedBook.available = false
がエラーになっているところから対処します。
先程idを"1"にするか1にするかで本が見つからずnullになってしまいエラーになりました。
selectedBook.availableが示しているのはnullの可能性があり同じことが起きるよと言っているわけです。
ここで利用できるのがType Defense
です。
このサイトを利用してまずは基本を学びます
user1 = {
name: "watanabe",
age: 27
}
user2 = {
name: "tanaka"
}
function getAge(user) {
return user.age
}
console.log(getAge(user1))
console.log(getAge(user2))
ここでuser2のageはundefinedになってしまいます。
そこで以下のようにガードすることで防ぐことが可能です
user1 = {
name: "watanabe",
age: 27
}
user2 = {
name: "tanaka"
}
function getAge(user) {
if(!user.age) {
console.error("年齢がありません")
}
return user.age
}
console.log(getAge(user1))
console.log(getAge(user2))
これを利用することで先程のエラーも消すことが可能です。
const books = [
{ title: "TypeScript入門", author: "田中太郎", available: true },
{ title: "JavaScript基礎", author: "山田花子", available: false },
{ title: "Reactと実践", author: "鈴木一郎", available: true }
]
const borrowedBooks = []
let newBookId = 1
function addNewBook(book) {
books.push(book)
return book
}
function borrowedBook(title) {
const selectedBook = books.find(book => book.title === title && book.available )
if (!selectedBook) {
console.error("貸し出しできる本が見つかりませんでした")
return
}
selectedBook.available = false
const newBorrowedBook = { id: newBookId++, book: selectedBook, status: "borrowed" }
borrowedBooks.push(newBorrowedBook)
return selectedBook
}
function returnBook(bookId) {
const selectedBook = borrowedBooks.find(book => book.id === bookId)
if (!selectedBook) {
console.error("返却する本が見つかりませんでした")
return
}
selectedBook.book.available = true
selectedBook.status = "returned"
return selectedBook
}
addNewBook({ title: "Pythonで学ぶデータサイエンス", author: "伊藤花子", return: true })
addNewBook({ title: "Vue.js入門", author: "鈴木一郎", return: true })
borrowedBook("TypeScript入門")
returnBook("1")
console.log(books)
console.log(borrowedBooks)
これでnullに対してアクセスされることはなくなったのでより安全になりました。
型宣言をする
次に明示的に型を宣言する方法について紹介します。
const myName: string = "watanabe"
const age: number = 27
const isMen: boolean = true
エディタでは型を明示的にかかなくても推論してくれる機能がありますが、このように型を書くことも可能です。
myNameなどに型を書いておくのは推論でも問題ないことが多いですが、以下のようなときに役立ちます
function printAge(age) {
console.log(age + "歳です")
}
console.log(27)
console.log("watanabe")
このとき関数は本来であれば年齢なのでnumber
がくるはずですが、ageの型が指定されていないため、どんな型(any)でも受け付ける関数になっています。
このようなときに型を明示的に書くことが可能です。
function printAge(age: number) {
console.log(age + "歳です")
}
console.log(27)
console.log("watanabe") // type error
それでは書籍管理システムに関数の引数の型を指定しましょう
const books = [
{ title: "TypeScript入門", author: "田中太郎", available: true },
{ title: "JavaScript基礎", author: "山田花子", available: false },
{ title: "Reactと実践", author: "鈴木一郎", available: true }
]
const borrowedBooks = []
let newBookId = 1
function addNewBook(book: { title: string, author: string, return: boolean }) {
books.push(book)
return book
}
function borrowedBook(title: string) {
const selectedBook = books.find(book => book.title === title && book.available )
if (!selectedBook) {
console.error("貸し出しできる本が見つかりませんでした")
return
}
selectedBook.available = false
const newBorrowedBook = { id: newBookId++, book: selectedBook, status: "borrowed" }
borrowedBooks.push(newBorrowedBook)
return selectedBook
}
function returnBook(bookId: number) {
const selectedBook = borrowedBooks.find(book => book.id === bookId)
if (!selectedBook) {
console.error("返却する本が見つかりませんでした")
return
}
selectedBook.book.available = true
selectedBook.status = "returned"
return selectedBook
}
addNewBook({ title: "Pythonで学ぶデータサイエンス", author: "伊藤花子", return: true })
addNewBook({ title: "Vue.js入門", author: "鈴木一郎", return: true })
borrowedBook("TypeScript入門")
returnBook("1")
console.log(books)
console.log(borrowedBooks)
すると先程問題になっていたreturnBook("1")
がtype errorになりました
これで型が違うものが関数に渡されることは防がれました。
文字列から数値に変えて次に進みましょう
returnBook(1)
オブジェクトの型を用意する
次にオブジェクトの型について紹介します
type Menu = {
name: string,
price: number
}
const menu1 = {
name: "ラーメン",
price: 1000
}
const mune2 = {
name: "チャーハン",
prise: 800
}
このようにオブジェクトの型を宣言することができます
では実際にMenu型を明示的に宣言していきます
type Menu = {
name: string,
price: number
}
const menu1: Menu = {
name: "ラーメン",
price: 1000
}
const mune2: Menu = {
name: "チャーハン",
prise: 800
}
すると以下のようにエラーがでました
気づいた方もいるかもしれませんが、なんとprice
のつづりがprise
になっていました
こういう間違えも型を使うことで気づくことができます。
では、実際にbookの引数をBook型に変更してみましょう
const books = [
{ title: "TypeScript入門", author: "田中太郎", available: true },
{ title: "JavaScript基礎", author: "山田花子", available: false },
{ title: "Reactと実践", author: "鈴木一郎", available: true }
]
type Book = {
title: string,
author: string,
available: boolean
}
const borrowedBooks = []
let newBookId = 1
function addNewBook(book: Book) {
books.push(book)
return book
}
スッキリしたコードになりました。
しかし問題が発生していることがわかりました
addNewBookをBook型にしたことで、オブジェクトの中身が間違っていました
return
ではなくavailable
が正しかったのです。
ここも修正しておきましょう
addNewBook({ title: "Pythonで学ぶデータサイエンス", author: "伊藤花子", available: true })
addNewBook({ title: "Vue.js入門", author: "鈴木一郎", available: true })
ネストしたオブジェクトの型とnull許容
オブジェクトの中にオブジェクトがある場合は以下のように書くことが可能です
type Menu = {
name: string,
price: number,
kind: {
karame: boolean,
abura: string,
yasai: string
}type Menu = {
name: string,
price: number,
kind: {
katame: string,
koime: string,
yasai: string
}
}
const menu1: Menu = {
name: "次郎ラーメン",
price: 1000,
option: {
katame: "普通",
koime: "濃いめ",
yasai: "少なめ"
}
}
const mune2: Menu = {
name: "チャーハン",
price: 800
}
}
const menu1: Menu = {
name: "次郎ラーメン",
price: 1000,
option: {
abura: "多め",
karame: true,
yasai: "少なめ"
}
}
const mune2: Menu = {
name: "チャーハン",
price: 800
}
ここではラーメンを次郎ラーメンとしてみました。
次郎ラーメンではトッピングを無料でできるシステムがあり
- 味の濃さ
- 脂の量
- 野菜の量
を選択できます。このようそもオブジェクトに持たせるように実装を変更しました。
ここで問題が出てきます。それはチャーハンにはこのようなオプションは存在しないのです。
このときにはkind
の部分だけ別オブジェクトにして、その型をnull許容にすることで回避できます
type Option = {
abura: string,
karame: string,
yasai: string
}
type Menu = {
name: string,
price: number,
option?: Option
}
const menu1: Menu = {
name: "次郎ラーメン",
price: 1000,
option: {
abura: "増し",
karame: true,
yasai: "少なめ"
}
}
const mune2: Menu = {
name: "チャーハン",
price: 800
}
このようにOption型
を用意して?
をつけることでoptionにはいるのはOption型またはnullになるようにしました。
チャーハンにはオプションがないのでoptionの項目を埋めなくてもエラーは起きません
では書籍管理システムの貸出を表すborrwoedBookの型を定義してみます
type BorrowedBook = {
id: number,
book: Book,
status: string
}
配列の型について
配列の中身に関しても型を宣言することは可能です
const ids = [100, 200]
// const ids: number[] = [100, 200]
ids.push(true)
たとえば数字だけの配列の場合、trueをいれようとすると型でエラーになります。
これは推測でnumberの配列としているからです。
明示的に型を書くことも可能です number[]
それでは実際にレンタルした本を表すborrowedBooks
に型をつけてみましょう
type BorrowedBook = {
id: number,
book: Book,
status: string
}
const borrowedBooks: BorrowedBook[] = []
リテラル型とユニオン型
リテラル型は特定の値だけを代入できる型を表現するものです
const myName: "watanabe" = "watanabe"
const myName2: "watanabe" = "tanaka" // error
このようにmyNameを"watanabe"型にしたので、それ以外の文字列を入れることはできません
これをどんなところで役に立つの?と思う方もいるかもしれませんが、例えば「スロットのボタン」を表すような変数を用意したとき
const slotButton: 1 | 2 | 3 = 1
このように書くと1,2,3しかいれられない変数になります。
またこのように|
で区切ることをユニオン型といいます。ORのように思っていただければ大丈夫です。
書籍管理システムでは借りた本のステータスをborrowed
とreturn
で表していましたが、ここも自由度が高く実装によっては他の文字列も入ってしまうのでユニオン型にしてみましょう
type BorrowedBook = {
id: number,
book: Book,
status: "borrowed" | "returned"
}
Narrowing
ここでは型の絞り込みであるnarrowingを説明していきます。
本来書籍にはidを割り振るのが一般的です。なのでまずはidを追加して初期データを直しましょう
const books = [
{ id: 1, title: "TypeScript入門", author: "田中太郎", available: true },
{ id: 2, title: "JavaScript基礎", author: "山田花子", available: false },
{ id: 3, title: "Reactと実践", author: "鈴木一郎", available: true }
]
type Book = {
id: number,
title: string,
author: string,
available: boolean
}
(省略)
addNewBook({ id: 4, title: "Pythonで学ぶデータサイエンス", author: "伊藤花子", available: true })
addNewBook({ id: 5, title: "Vue.js入門", author: "鈴木一郎", available: true })
では新しい関数を作成していきます。
書籍を検索してくれる関数でid
とタイトル
どちらでも検索してくれるとしましょう
function getBookDetail(identifier: string | number) {
if (typeof identifier === "string") {
return books.find(book => book.title === identifier)
} else {
return books.find(book => book.id === identifier)
}
}
getBookDetail("TypeScript入門")
getBookDetail(1)
このようにすれば文字列でも数値でも問題なく検索結果を返してくれるようになります。
型を使ってifの条件を変えることが可能です
最後のエラーをなくす
JavaScriptからTypeScriptに移行してきましたが、まだエラーが出ています。
ホバーしてみると
Argument of type '{ id: number; book: { id: number; title: string; author: string; available: boolean; }; status: string; }' is not assignable to parameter of type 'BorrowedBook'.
Types of property 'status' are incompatible.
Type 'string' is not assignable to type '"borrowed" | "returned"'
このようなエラーが発生しています。
私達には同じ型に見えますが、ここは明示的に型を宣言する必要があります。
const newBorrowedBook: BorrowedBook = { id: newBookId++, book: selectedBook, status: "borrowed" }
borrowedBooks.push(newBorrowedBook)
これでエラーが消えました
関数の戻り値の型
作成したgetBookDetailsを別のファイルに移動します
$ touch function.ts
export function getBookDetail(identifier: string | number, books: Book[]) {
if (typeof identifier === "string") {
return books.find(book => book.title === identifier)
} else {
return books.find(book => book.id === identifier)
}
}
ここでBook型が使えないので、これも外部ファイルに出しておきましょう
$ touch book.ts
export type Book = {
id: number,
title: string,
author: string,
available: boolean
}
index.tsとfunction.tsでもインポートしといてください
では関数の戻り値の型をみてみます
戻り値としてはundefinedの可能性があると言われています。
つまりbookが見つからない可能性があるわけです。
ここをBook型が変えるように直します
import { Book } from "./book"
export function getBookDetail(identifier: string | number, books: Book[]): Book {
if (typeof identifier === "string") {
return books.find(book => book.title === identifier)
} else {
return books.find(book => book.id === identifier)
}
}
そうすると次はreturnでエラーがでました
もし見つからない場合undefinedが帰るのでBookを返せないことからエラーになります
今回は見つからない場合、エラーをスローするようにしてそれ以降プログラムが動かないようにします
import { Book } from "./book"
export function getBookDetail(identifier: string | number, books: Book[]): Book {
const book = typeof identifier === "number" ? books.find(book => book.id === identifier) : books.find(book => book.title === identifier)
if (!book) {
throw new Error("本が見つかりませんでした")
}
return book
}
これでBookが返せないときは例外を出すようになったので型エラーは消えました
関数の戻り値がないときの型Void
function addNewBook(book: Book) {
books.push(book)
}
addNewBookをすこし変えてみました。
このときの関数の戻り値は存在しません。こういうときはvoid型を指定します
function addNewBook(book: Book): Void {
books.push(book)
}
ユーティリティ型について
ユーティリティ型(utility type)は、型から別の型を導き出してくれる型です。
型における関数のようなイメージです。
1. Partialについて
Partialはオブジェクトがもつすべてのプロパティをオプショナルにするものです
type User = {
id: number;
name: string;
age: number;
}
let users: User[] = [
{ id: 1, name: "Alice", age: 25 },
{ id: 2, name: "Bob", age: 30 },
{ id: 3, name: "Charlie", age: 35 }
];
function updateUser(id: number, changes: Partial<User>): void {
const user = users.find(user => user.id === id);
if (!user) {
console.log("User not found!");
return;
}
const updatedUser = { ...user, ...changes };
users = users.map(u => u.id === id ? updatedUser : u);
console.log(`User ${id} updated:`, updatedUser);
}
updateUser(1, { name: "Alice Cooper" }); // name のみを更新
updateUser(2, { age: 31 }); // age のみを更新
updateUser(4, { name: "Dave" }); // 存在しないユーザー (エラー)
このようにユーザー情報の一部だけを変更する関数に使うことで、ユーザーすべてのプロパティをもっていない引数を受けることができます
2. Omitについて
Omitは任意のプロパティを除いてオブジェック型を作るものです
type Person = {
id: number;
name: string;
age: number;
location: string;
email: string;
}
// age と location プロパティを除外した新しい型を定義
type PersonSummary = Omit<Person, 'age' | 'location'>;
function displayPersonSummary(person: PersonSummary) {
console.log(`ID: ${person.id}, Name: ${person.name}, Email: ${person.email}`);
}
const person: Person = {
id: 1,
name: "Alice",
age: 30,
location: "Tokyo",
email: "alice@example.com"
};
const summary: PersonSummary = {
id: person.id,
name: person.name,
email: person.email
};
displayPersonSummary(summary); // 出力: ID: 1, Name: Alice, Email: alice@example.com
書籍管理アプリにOmitを使う
いまは新しい本の追加で本のidを渡していますがここを修正します
import { Book } from "./book"
import { getBookDetail } from "./function"
const books = [
{ id: 1, title: "TypeScript入門", author: "田中太郎", available: true },
{ id: 2, title: "JavaScript基礎", author: "山田花子", available: false },
{ id: 3, title: "Reactと実践", author: "鈴木一郎", available: true }
]
type BorrowedBook = {
id: number,
book: Book,
status: "borrowed" | "returned"
}
const borrowedBooks: BorrowedBook[] = []
let newBookId = 1
function addNewBook(book: Omit<Book, "id">): void {
const newBook = {
id: newBookId++,
...book
}
books.push(newBook)
}
function borrowedBook(title: string) {
const selectedBook = books.find(book => book.title === title && book.available )
if (!selectedBook) {
console.error("貸し出しできる本が見つかりませんでした")
return
}
selectedBook.available = false
const newBorrowedBook: BorrowedBook = { id: newBookId++, book: selectedBook, status: "borrowed" }
borrowedBooks.push(newBorrowedBook)
return selectedBook
}
function returnBook(bookId: number) {
const selectedBook = borrowedBooks.find(book => book.id === bookId)
if (!selectedBook) {
console.error("返却する本が見つかりませんでした")
return
}
selectedBook.book.available = true
selectedBook.status = "returned"
return selectedBook
}
addNewBook({ title: "Pythonで学ぶデータサイエンス", author: "伊藤花子", available: true })
addNewBook({ title: "Vue.js入門", author: "鈴木一郎", available: true })
borrowedBook("TypeScript入門")
returnBook(1)
getBookDetail("TypeScript入門", books)
console.log(books)
console.log(borrowedBooks)
omitをつかうことでidを減らして、最初に定義しているnewBookIdをインクリメントすることにしました
function addNewBook(book: Omit<Book, "id">): void {
const newBook = {
id: newBookId++,
...book
}
books.push(newBook)
}
ジェネリクス
以下のようにランダムで引数のどちらかを返してくれる関数があります
function chooseRandomlyString(v1: string, v2: string): string {
return Math.random() <= 0.5 ? v1 : v2;
}
function chooseRandomlyNumber(v1: number, v2: number): number {
return Math.random() <= 0.5 ? v1 : v2;
}
function chooseRandomlyURL(v1: URL, v2: URL): URL {
return Math.random() <= 0.5 ? v1 : v2;
}
引数の型が違うだけで同じ内容なのでまとめようとします
function chooseRandomly2(v1: any, v2: any): any {
return Math.random() <= 0.5 ? v1 : v2;
}
let str = chooseRandomly2(0, 1);
str = str.toLowerCase();
しかしこの場合数値をいれてしまうとtoLowerCase
をもっていないのでエラーになります
ここをうまく解消できるのがジェネリクスです
function chooseRandomly3<T>(v1: T, v2: T): T {
return Math.random() <= 0.5 ? v1 : v2;
}
let str2 = chooseRandomly3<string>(0, 1); // error
const str3= str2.toLowerCase();
このように書くことで使うときに関数がうけとる引数の型を決めることができます。
そうすることでchooseRandomly3<string>(0, 1)
の時点でエラーにできるため、toLowerCaseが呼ばれる心配はなくなりました
書籍管理システムのnewAddBook関数を共通化してみます
// function addNewBook(book: Omit<Book, "id">): void {
// const newBook = {
// id: newBookId++,
// ...book
// }
// books.push(newBook)
// }
function addArray<T>(array: T[], value: T): T[]{
array.push(value)
return array
}
// addArray({ title: "Pythonで学ぶデータサイエンス", author: "伊藤花子", available: true })
addArray(books, { id: 1, title: "Vue.js入門", author: "鈴木一郎", available: true } )
このように共通化して配列に対して追加できる関数を作れました
おわりに
こちらの記事はYoutubeでもハンズオンを投稿していますのでよければご覧ください!
反応がよかったらまた別の完全版を作りますのでぜひコメントにください!
ここまで読んでいただけた方はいいねとストックよろしくお願いします。
@Sicut_study をフォローいただけるととてもうれしく思います。
また明日の記事でお会いしましょう!
JISOUのメンバー募集中
プログラミングコーチングJISOUではメンバーを募集しています。
日本一のアウトプットコミュニティでキャリアアップしませんか?
気になる方はぜひHPからライン登録お願いします👇