はじめに
この記事はそれなりにPythonを使ったことのある人間がとりあえずTypeScriptを書き始めることが出来るレベルになることを目標としています。
Python側の事情もそこそこに書いたのでもしかしたらTypeScriptユーザーのためのPython入門にもなる、かもしれません。
Why Python and TypeScript?
ヨーロッパ圏の言語を勉強する際に似た系統の言語を一緒に覚えると学習効率がいいという話を聞いたことがありませんか?
プログラミング言語も同じで、新しい言語を学習する際は似た言語に絡めて覚えると習得が早いような気がしています。
近年PythonはTypeScriptを参考に型システムを拡張しており近しい仕組みが数多く存在します。またそれ以外にも同じスクリプト言語であり、クラスベースであり、関数が第一級オブジェクトであり、総合的に見て似たような言語だと言えそうです。ユースケースも一部被ってますしね。
なので今回はこの2つの言語を例に説を立証してみようと思いました。
被検体は私(Python歴3年、TS歴3ヶ月)、そしてあなたです。よろしくお願いします。
開発環境
実行環境
Pythonみたいにとりあえずサクッとコンソールで動かせる環境が欲しい場合はnode + ts-node(後述)がいいかと思います。
ただじゃあ早速node.jsをインストールして…というのはちょっと待ってください。
あなたはそれをやってPythonで痛い目を見たことがありませんか?
実行環境のバージョン管理
これです。どうせ実行環境は複数用意することになるので最初から入れておきましょう。
現状のデファクトスタンダードはnvmですが最近はVoltaも人気っぽいですね。
パッケージ管理
npmとyarnという二大派閥があります。PipenvとPoetryみたいなもんです。
とりあえず作るぞ
というわけでとりあえず最もシンプルな構成(nvm + node + npm + ts-node)で実行環境を作っていきます。
まずnvm。UNIX系の方は以下の指示に従ってスクリプトを実行してください。
Windowsはここから最新のnvm-setup.zipをダウンロードしてインストールしましょう。
以下は共通です。node + npm(+ npx)のインストール。
$ nvm install lts # 安定バージョンのインストール。最新バージョンを入れたい場合は`latest`
$ nvm use xx.xx.x # ↑のコマンドを実行したときに指示がなければ必要なし
プロジェクトのフォルダを作ったら移動してtypescriptとts-nodeをインストール。
$ npm init -y
$ npm install typescript ts-node -D # dev-dependencies
Hello, world!
$ npx ts-node
> console.log("Hello, world!")
Hello, world!
こんにちは、世界!
余談: ts-nodeってなに?
お気付きかと思いますが実はnvmもnodeもnpmもTypeScriptではなくJavaScriptのお話でした。
試しに適当な場所でJSのハロワをしてみましょう。
$ node
> console.log("Hello, world!")
Hello, world!
出来ましたね。
本来TSを実行する際は.tsファイルから.jsファイルを生成してnodeに与える必要があるのですが、
それをショートカットするためプロジェクトにインストールするものがts-nodeです1。
よってts-nodeを入れたプロジェクト以外では直でTSを使うことは出来ないのでご注意ください。
エディター(IDE) / リンター / フォーマッター
この3つはもう切っても切れない関係なのでセットで考えることをお勧めします。
つまりこのリンター/フォーマッターは自分が使っているエディターで動くのか?ということを考えましょうということですね。
PythonマンはVS Codeを使っていることが多いはずなのでここではVS Codeでリンター/フォーマッターを動かすことを考えます。
リンター
リンターはESLintの一強です。
$ npm install eslint -D
入れたらまず設定ファイルを書かねばならないのですが、ひとまず置いておいて次へ進みましょう。
フォーマッター
フォーマッターについては議論があります。ESLintのフォーマット機能を使うか、prettierを使うかです。
prettierは細かいカスタマイズを極力許さない思想が特徴のフォーマッターであり、分かりやすく言えばPythonにおけるblack的なポジションです(blackほど厳しくはないですが)。
私はblackの信徒なのでprettierを入れます。
$ npm install prettier -D
blackとflake8がぶつかるように、prettierもESLintとぶつかることがあるのでこれを解消するためにeslint-config-prettierも入れます。
$ npm install eslint-config-prettier -D
設定ファイル
設定ファイルの作成を後回しにしたのはフォーマッターに何を使うかによって設定が変わるためです。
具体的に言うと
$ npx eslint --init
コマンドでESLintと対話を始めた際の最初の質問、
How would you like to use ESLint?
に対する答えがESLintを使う場合は
> To check syntax, find problems, and enforce code style
に、prettierを使う場合は
> To check syntax and find problems
となります。
その他の問いに対しては今回はこんな感じになるかと↓
これでプロジェクトのルートに.eslintrc.yml
が生成されますが、prettierを使う場合はさらにもうひと手間。
extends: # この項目の最後に
- eslint:recommended
...
- prettier # これを追加します
また同じくルートに.prettierrc.yml
を作ることでprettierrcの設定が可能ですが、前述の通りprettierは細かいカスタマイズを良しとしないため極力項目は少なくしましょう。
printWidth: 100
semi: false # 行末のセミコロンだけは許せないので
参考:
TypeScript プロジェクトに ESLint を導入する - まくろぐ
TypeScript コードを Prettier で自動整形する - まくろぐ
そして最後にもう1つ重要な設定ファイルを作ります。
$ npx tsc --init # 成功するとルートにtsconfig.jsonが生成されます
これが何かとか、どんな設定があるのかとかは今は考えなくて大丈夫です。調べるとげんなりしちゃうので。
ただしこのコマンドだけは今やっておかないとnull安全でないTypeScriptを書くことになるので必ず実行しておいてください。
###VS Codeの設定
ESLint、prettierそれぞれの拡張機能をインストールして設定を書きます。
{
"typescript.format.enable": false,
"[typescript]": {
"editor.formatOnSave": true,
"editor.formatOnType": true,
"editor.formatOnPaste": true,
},
}
"typescript.format.enable": false
はVS Code標準のTSフォーマッターを切る設定なので必須、下3行はお好みです。
適当なファイルを作って正しく動作しているか確認してみましょう。
OK.
prettierが正しく動いているかどうかは設定ファイルを弄ってみると分かりやすいです。
semi: true # 変更
参考:
VS Code のフォーマッターで自動整形する (editor.formatOnSave) - まくろぐ
デバッグ
Pythonを書いていたときは何も考えずにF5を押せばデバッグが出来ていたと思いますが、TSの場合は少しだけ設定が必要です。
{
// IntelliSense を使用して利用可能な属性を学べます。
// 既存の属性の説明をホバーして表示します。
// 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Current File",
"type": "python",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal"
}
]
}
{
"version": "0.2.0",
"configurations": [
{
"name": "TypeScript: Current File",
"type": "pwa-node",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"],
"skipFiles": ["<node_internals>/**", "${workspaceRoot}/node_modules/**"],
}
]
}
Pythonの時に比べて増えたのは下2行ですね。これでts-nodeを経由してnodeでデバッグすることができます。
ブレークポイントも設定可能です。
テンプレート
ここまでの内容をまとめたテンプレートリポジトリを用意しました。
各ファイルの置き場所確認などにご活用ください。
基本文法
ここからはいよいよ仕様や文法の比較に入ります。まずは構造化プログラミングにおける基本の基から。
なお以降JSの機能もまとめてTSと表記します(呼びわけが面倒なので)がその点ご容赦ください。
if
Pythonではif
文を1行で書くとblackに殴られますがprettierには怒られません。
import random
if random.random() > 0.5:
print("big")
# ng(blackが許さないだけで文法的に間違いではありません)
if random.random() > 0.5: print("big")
if (Math.random() > 0.5) {
console.log("big")
}
// ok
if (Math.random() > 0.5) console.log("big")
論理演算子
短絡評価であり最後に評価した値を返す仕様はTSも同様です。
print(0 and 1) # falseyな「値」が返る
> 0
print(0 and "") # 両辺がfalseyの場合は左辺が返る
> 0
print(1 and "a") # 両辺がtruthyの場合は右辺が返る
> a
console.log(0 && 1)
> 0
console.log(0 && "")
> 0
console.log(1 && "a")
> a
print(0 or 1) # truthyな「値」が返る
> 1
print(0 or "") # 両辺がfalseyの場合は右辺が返る
>
print(1 or "a") # 両辺がtruthyの場合は左辺が返る
> 1
console.log(0 || 1)
> 1
console.log(0 || "")
>
console.log(1 || "a")
> 1
print(not (1 or "a")) # 否定をつけるとbool値になる
> False
console.log(!(1 || "a"))
> false
truthy / falsey
Pythonでは自作オブジェクトの真偽を自分で決められましたが、TSにおいてオブジェクトは常に真です。
つまり何がtruthyで何がfalseyかは全て事前に決まっているということですね。
TypeScript Deep Dive 日本語版様より便利な表を引用します。
変数の型 | falsyな値 | truthyな値 |
---|---|---|
boolean |
false |
true |
string |
'' (空文字列) |
その他の文字列 |
number |
0 NaN
|
その他の数値 |
null |
常にfalsy | なし |
undefined |
常にfalsy | なし |
その他のオブジェクト ( {} や[] といった空のものも含む) |
なし | 常にtruthy |
せっかくなので残りのプリミティブ型も表に加えましょう。
(説明が前後しますがTSにおける値はプリミティブ型とオブジェクト型とに大別され、プリミティブ型はここに挙げた8種で全てです。)
変数の型 | falsyな値 | truthyな値 |
---|---|---|
function |
なし | 常にtruthy |
bigint |
0n |
その他の数値 |
symbol |
なし | 常にtruthy |
だいたいPythonと同じですね。[]
や{}
がtruthyなことだけ注意が必要そうです。
比較演算子
TSの比較演算子は事実上1つしかないので若干不便です。
hoge = []
fuga = []
print(hoge == fuga) # ==はオブジェクトが等価であるかを見るためtrue
> true
print(hoge is fuga) # isはオブジェクトが同一であるか(参照値が同じか)を見るためfalse
> false
const hoge = []
const fuga = []
console.log(hoge === fuga) // ===でオブジェクト同士を比較する場合は同一であるかを見る(Pythonのisと同じ)
> False
console.log(1 === 1) // プリミティブ同士を比較する場合は等価であるかを見る(Pythonの==と同じ)
> True
じゃあオブジェクト同士が同じ値を持つかどうかはどうやって?という感じなんですが、
どうやらdeep-equalというサードパーティ製ライブラリを使うらしい…。
本当ですか?どなたか他の方法をご存じでしたら教えてください。
三項演算子
PythonとTSでは式の順番が異なりますがやることは同じです。
import random
"big" if random.random() > 0.5 else "small"
Math.random() > 0.5 ? "big" : "small"
for
Pythonのfor
はTSで言うとfor...of
になります。
list_ = ["hoge", "fuga", "piyo"]
for i in range(len(list_)):
print(i)
> 0
> 1
> 2
for elm in list_:
print(elm)
> hoge
> fuga
> piyo
for i, elm in enumerate(list_):
print(i, elm)
> 0 hoge
> 1 fuga
> 2 piyo
const list_ = ["hoge", "fuga", "piyo"]
for (let i = 0; i < list_.length; i++) {
console.log(i)
}
> 0
> 1
> 2
for (const elm of list_) {
console.log(elm)
}
> hoge
> fuga
> piyo
list_.forEach((elm, i) => {
console.log(i, elm)
})
> 0 hoge
> 1 fuga
> 2 piyo
// {}は省略してもok
for (let i = 0; i < list_.length; i++) console.log(i)
for (const elm of list_) console.log(elm)
list_.forEach((elm, i) => console.log(i, elm))
TSのループ文はたくさんあるので詳しくはこちらをご確認ください。
ちなみにPythonで言うfor...else
に該当する構文はありません2。
変数
定義
ここはさすがにベースが弱い動的型付けであるPythonと言語仕様として強い静的型付けであるTSとの差を感じます。
hoge = "hoge" # 変数は突然代入してよい
hoge = 1 # ok
fuga: str = "fuga" # fugaをstr型として定義したので
fuga = 2 # これはng
from typing import Final
piyo: Final = "piyo" # 再代入を禁止する書き方も一応あります
piyo = "piyopiyo" # ng
let hoge = "hoge" // この定義でhogeはstring型になる(型推論)ので
hoge = 1 // 異なる型は代入できなくなる
let fuga: string = "fuga" // 上記の理由から`: string`を書く必要がないのでこの書き方はng
const piyo = "piyo" // 再代入禁止
piyo = "piyopiyo" // ng
基本的には全部const
で定義してどうしても再代入したい場合だけlet
を使うといいと思います。
あとvar
というのもあるのですが後方互換のために残してあるだけなので忘れてください。
スコープ
Pythonのスコープを切るものはモジュール/クラス/関数の3つ、TSのスコープを切るものはブロック3というとなんか違うように感じますが実際にはPythonよりちょっと狭いくらいで基本的な考え方は一緒です。
list_ = ["hoge", "fuga", "piyo"]
for elm in list_:
elm
print(elm) # elmは生きてるのでエラーにならない(今確認してみたらPylanceには怒られるようになってました。いつから?)
> piyo
const list_ = ["hoge", "fuga", "piyo"]
for (const elm of list_) {
elm
}
console.log(elm) // elmはブロック内で死んでるのでエラー
双方ともスコープ外の変数は巻き上げられる。
def print_hoge() -> None:
print(hoge)
hoge = "hoge"
print_hoge()
> hoge
function logHoge(): void {
console.log(hoge)
}
const hoge = "hoge"
logHoge()
> hoge
トップレベルで定義したものの扱いだけが若干面倒で、
- そのファイル(.ts)内に
import
もしくはexport
が1つでも書かれている場合はモジュールレベル(他のファイルから見えない) - 1つもない場合はグローバル(他のファイルから見える)
になります。
↑ hoge
とlogHoge
がよそから見えている
↑ hoge
だけが見えていてlogHoge
が見えなくなった
Pythonでバシバシトップレベルで定義することに慣れている方はご注意ください。
関数
定義
def log(msg: str) -> None:
print(msg)
function log(msg: string): void {
console.log(msg)
}
基本構文は同じですね。TSの方は戻り値がvoid
と書いてありますが実際にはundefined
が返ります。
色々な引数
# オプション引数
def log1(msg: str | None = None) -> None:
print(msg)
# デフォルト引数
def log2(msg: str = "hoge") -> None:
print(msg)
# キーワード引数
def log3(*, msg: str) -> None:
print(msg)
# 可変長引数
def log4(*msgs: str) -> None:
print(msgs)
# 可変長キーワード引数
def log5(**msgs: str) -> None:
print(msgs)
// オプション引数
function log1(msg?: string): void {
console.log(msg)
}
// デフォルト引数
function log2(msg = "hoge"): void {
console.log(msg)
}
// キーワード引数
function log3(option: { msg: string }): void {
console.log(option.msg)
}
// 可変長引数
function log4(...msgs: string[]): void {
console.log(msgs)
}
// 可変長キーワード引数
function log5(option: object): void {
console.log(option)
}
引数の渡され方
関数の仮引数には何が渡されるかと言うと、これもPythonと同じく参照値が渡されます。
参照の値渡しとか共有渡しとか呼ばれるアレです。
よってオブジェクトを渡した場合には副作用が生じる可能性があります。
def log(msgs: list[str]) -> None:
msgs.append("piyo")
print(msgs)
msgs = ["hoge", "fuga"]
log(msgs)
> ["hoge", "fuga", "piyo"]
log(msgs)
> ["hoge", "fuga", "piyo", "piyo"]
function log(msgs: string[]): void {
msgs.push("piyo")
console.log(msgs)
}
const msgs = ["hoge", "fuga"]
log(msgs)
> [ "hoge", "fuga", "piyo" ]
log(msgs)
> [ "hoge", "fuga", "piyo", "piyo" ]
無名関数
Pythonのlambda
は引数に型を書くことが出来ないのですが、TSはアロー関数の引数にも型をつけることが出来ます(実際には型推論が優秀であまり書くことはないですが)。
lambda x: print(x)
(x: string) => console.log(x)
高階関数との組み合わせ↓
seq = [1, 2, 3, 4, 5]
print(list(filter(lambda x: x % 2 == 0, seq)))
> [2, 4]
const seq = [1, 2, 3, 4, 5]
console.log(seq.filter((x) => x % 2 === 0))
> [ 2, 4 ]
nullとundefined
どっち使えばいい?
MSのTypeScriptチームはnull
を使わないそうです。
実際のところ
上記に倣ってnull
は使わない、ただしnullチェックはif (tgt != null)
で行うというプロジェクトが多いみたいです(==
はnull
とundefined
を区別しないため)。またこれがTSで==
演算子を使う唯一の機会でもあります。
nullにまつわる構文
最近のTSはかなりnull周りが書きやすくなっている印象があります。
以下の構文は両方ともPythonには存在しません。
// オプショナルチェーン
function len(text?: string): void {
const len = text?.length // textがundefinedの場合undefinedになる
console.log(len)
}
// Null合体代入演算子
function echo(text: string, options: { big?: boolean; trim?: boolean }): void {
options.big ??= true // bigがundefinedの場合trueを代入
options.trim ??= true
let processed = text
if (options.big) processed = processed.toUpperCase()
if (options.trim) processed = processed.trim()
console.log(processed)
}
列挙体(enum)とunion型
これもどちらを使うべきかという議論がありますが、私が調べた限り
さようなら、TypeScript enum | Kabuku Developers Blog
これが結論なように見えました。
ちなみにここで触れた通りPythonのLiteral型は反復処理ができないため全てのEnumを駆逐するまでには至りません(頑張ってこねこねすればいけそうですが…)。
クラス
定義
from dataclasses import dataclass
@dataclass
class Player:
name: str
age: int
def rename(self, name: str) -> None:
self.name = name
me = Player("nicco", 29)
class Player {
constructor(public name: string, public age: number) {}
rename(name: string): void {
this.name = name
}
}
const me = new Player("mirai", 14)
Pythonの@dataclass
はフィールドの宣言を元に__init__
やその他特殊メソッドを自動で生成してくれるデコレータですが、TSでは逆にコンストラクタの引数にアクセス修飾子をつけることによってフィールドの宣言を省略することができます。
またメソッド内でインスタンス変数にアクセスするためのself
はTSではthis
になります。
公称型と構造的部分型
PythonとTSはどちらも公称型と構造的部分型の両方を扱うことのできる言語です。
ただしPythonは構造的部分型も扱うことが出来る公称型がベースの言語であり
TSが公称型も扱うことが出来る構造的部分型がベースの言語であることは押さえておく必要があります。
それはなぜか。我々のような公称型に慣れた人間は公称型に準じた設計をしがちであり、そして構造的部分型はその影響範囲が広いからです。
参考:
TypeScript: 異なる2つの型システム「公称型」と「構造的部分型」
公称型に準じた設計とは
まさに上で書いたようなクラスがそれです。これを構造的部分型に準じた設計に変えてみましょう。
interface Player {
name: string
age: number
}
// `Player`型を含め`name`属性を持った型ならなんでも受け取れる
function rename(named: { name: string }, newName: string) {
named.name = newName
}
const me: Player = { name: "mirai", age: 14 }
rename(me, "hikari")
こうです。Goでよく見た感じになりましたね。
Protocolを継承しないと構造的部分型にならないPythonと異なり、TSで定義した型4は特定の条件を満たさない限り常に構造的部分型です。
今後{ name: string, age: number, rename: (name: string) => void }
が全て公称型に準じた型であるPlayer
と見なされてしまうのは設計者の意図に反する危ない状態ではないでしょうか?
じゃあどうするのが正解?
どちらかに寄せましょう。構造的部分型に準じた設計に変えるか、クラスを公称型で定義するかです。
後者のやり方は単純で、1つ以上パブリックではないフィールドを作ればOKです。
class Player {
constructor(private name: string, public age: number) {}
rename(name: string): void {
this.name = name
}
}
class Neko {
constructor(private name: string, public age: number) {}
rename(name: string): void {
this.name = name
}
}
let player: Player
player = new Neko("mirai", 2) // 代入できない!
おわりに
[タイムリープTypeScript 〜TypeScript始めたてのあの頃に知っておきたかったこと〜]
(https://qiita.com/advent-calendar/2021/timeleap-typescript)
カレンダー2日目の記事でした。
あの頃に知っておきたかったということでかなり基本に寄った内容でまとめてみましたがどうでしょうか?
特に環境周りは調べていて昔より随分楽になったんだな~と感じました。
また入門者の方はこれで一巡他の入門記事が読めるようになったのではないでしょうか。
おすすめは@uhyo氏のTypeScriptの型入門シリーズです。
本当にためになるシリーズなので未読の方はぜひ。