LoginSignup
98
121

More than 1 year has passed since last update.

はじめに

この記事はそれなりに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も人気っぽいですね。

パッケージ管理

npmyarnという二大派閥があります。PipenvPoetryみたいなもんです。

とりあえず作るぞ

というわけでとりあえず最もシンプルな構成(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

となります。
その他の問いに対しては今回はこんな感じになるかと↓
image.png
これでプロジェクトのルートに.eslintrc.ymlが生成されますが、prettierを使う場合はさらにもうひと手間。

./eslintrc.yml
extends:  # この項目の最後に
  - eslint:recommended
  ...
  - prettier  # これを追加します

また同じくルートに.prettierrc.ymlを作ることでprettierrcの設定が可能ですが、前述の通りprettierは細かいカスタマイズを良しとしないため極力項目は少なくしましょう。

./.prettierrc.yml
printWidth: 100
semi: false  # 行末のセミコロンだけは許せないので

参考:
TypeScript プロジェクトに ESLint を導入する - まくろぐ
TypeScript コードを Prettier で自動整形する - まくろぐ

そして最後にもう1つ重要な設定ファイルを作ります。

$ npx tsc --init  # 成功するとルートにtsconfig.jsonが生成されます

これが何かとか、どんな設定があるのかとかは今は考えなくて大丈夫です。調べるとげんなりしちゃうので。
ただしこのコマンドだけは今やっておかないとnull安全でないTypeScriptを書くことになるので必ず実行しておいてください。

VS Codeの設定

ESLintprettierそれぞれの拡張機能をインストールして設定を書きます。

./vscode/settings.json
{
    "typescript.format.enable": false,
    "[typescript]": {
        "editor.formatOnSave": true,
        "editor.formatOnType": true,
        "editor.formatOnPaste": true,
    },
}

"typescript.format.enable": falseはVS Code標準のTSフォーマッターを切る設定なので必須、下3行はお好みです。

適当なファイルを作って正しく動作しているか確認してみましょう。
image.png
image.png
OK.

prettierが正しく動いているかどうかは設定ファイルを弄ってみると分かりやすいです。

/.prettierrc.yml
semi: true  # 変更

この状態でsample.tsを保存すると…
image.png
大丈夫ですね。

参考:
VS Code のフォーマッターで自動整形する (editor.formatOnSave) - まくろぐ

デバッグ

Pythonを書いていたときは何も考えずにF5を押せばデバッグが出来ていたと思いますが、TSの場合は少しだけ設定が必要です。

多くの人がデフォルトのまま使っているであろうPythonプロジェクトのlaunch.json
{
    // 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"
        }
    ]
}
node+ts-nodeのlaunch.json(./.vscode/launch.json)
{
    "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でデバッグすることができます。
image.png
ブレークポイントも設定可能です。

テンプレート

ここまでの内容をまとめたテンプレートリポジトリを用意しました。
各ファイルの置き場所確認などにご活用ください。

基本文法

ここからはいよいよ仕様や文法の比較に入ります。まずは構造化プログラミングにおける基本の基から。
なお以降JSの機能もまとめてTSと表記します(呼びわけが面倒なので)がその点ご容赦ください。

if

Pythonではif文を1行で書くとblackに殴られますがprettierには怒られません。

py
import random

if random.random() > 0.5:
    print("big")

# ng(blackが許さないだけで文法的に間違いではありません)
if random.random() > 0.5: print("big")
ts
if (Math.random() > 0.5) {
  console.log("big")
}

// ok
if (Math.random() > 0.5) console.log("big")

論理演算子

短絡評価であり最後に評価したを返す仕様はTSも同様です。

py
print(0 and 1)  # falseyな「値」が返る
> 0
print(0 and "")  # 両辺がfalseyの場合は左辺が返る
> 0
print(1 and "a")  # 両辺がtruthyの場合は右辺が返る
> a
ts
console.log(0 && 1)
> 0
console.log(0 && "")
> 0
console.log(1 && "a")
> a
py
print(0 or 1)  # truthyな「値」が返る
> 1
print(0 or "")  # 両辺がfalseyの場合は右辺が返る
> 
print(1 or "a")  # 両辺がtruthyの場合は左辺が返る
> 1
ts
console.log(0 || 1)
> 1
console.log(0 || "")
> 
console.log(1 || "a")
> 1
py
print(not (1 or "a"))  # 否定をつけるとbool値になる
> False
ts
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つしかないので若干不便です。

py
hoge = []
fuga = []

print(hoge == fuga)  # ==はオブジェクトが等価であるかを見るためtrue
> true
print(hoge is fuga)  # isはオブジェクトが同一であるか(参照値が同じか)を見るためfalse
> false
ts
const hoge = []
const fuga = []

console.log(hoge === fuga)  // ===でオブジェクト同士を比較する場合は同一であるかを見る(Pythonのisと同じ)
> False
console.log(1 === 1)  // プリミティブ同士を比較する場合は等価であるかを見る(Pythonの==と同じ)
> True

じゃあオブジェクト同士が同じ値を持つかどうかはどうやって?という感じなんですが、
どうやらdeep-equalというサードパーティ製ライブラリを使うらしい…。
本当ですか?どなたか他の方法をご存じでしたら教えてください。

三項演算子

PythonとTSでは式の順番が異なりますがやることは同じです。

py
import random
"big" if random.random() > 0.5 else "small"
ts
Math.random() > 0.5 ? "big" : "small"

for

PythonのforはTSで言うとfor...ofになります。

py
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
ts
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との差を感じます。

py
hoge = "hoge"  # 変数は突然代入してよい
hoge = 1  # ok

fuga: str = "fuga"  # fugaをstr型として定義したので
fuga = 2  # これはng

from typing import Final
piyo: Final = "piyo"  # 再代入を禁止する書き方も一応あります
piyo = "piyopiyo"  # ng
ts
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よりちょっと狭いくらいで基本的な考え方は一緒です。

py
list_ = ["hoge", "fuga", "piyo"]

for elm in list_:
    elm

print(elm)  # elmは生きてるのでエラーにならない(今確認してみたらPylanceには怒られるようになってました。いつから?)
> piyo
ts
const list_ = ["hoge", "fuga", "piyo"]

for (const elm of list_) {
  elm
}

console.log(elm)  // elmはブロック内で死んでるのでエラー

双方ともスコープ外の変数は巻き上げられる。

py
def print_hoge() -> None:
    print(hoge)

hoge = "hoge"
print_hoge()
> hoge
ts
function logHoge(): void {
  console.log(hoge)
}

const hoge = "hoge"
logHoge()
> hoge

トップレベルで定義したものの扱いだけが若干面倒で、

  • そのファイル(.ts)内にimportもしくはexportが1つでも書かれている場合はモジュールレベル(他のファイルから見えない)
  • 1つもない場合はグローバル(他のファイルから見える)

になります。
image.png
hogelogHogeがよそから見えている
image.png
hogeだけが見えていてlogHogeが見えなくなった

Pythonでバシバシトップレベルで定義することに慣れている方はご注意ください。

参考:
ES2015のモジュール管理 | Think IT

関数

定義

py
def log(msg: str) -> None:
    print(msg)
ts
function log(msg: string): void {
  console.log(msg)
}

基本構文は同じですね。TSの方は戻り値がvoidと書いてありますが実際にはundefinedが返ります。

色々な引数

py
# オプション引数
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)
ts
// オプション引数
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と同じく参照値が渡されます。
参照の値渡しとか共有渡しとか呼ばれるアレです。
よってオブジェクトを渡した場合には副作用が生じる可能性があります。

py
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"]
ts
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はアロー関数の引数にも型をつけることが出来ます(実際には型推論が優秀であまり書くことはないですが)。

py
lambda x: print(x)
ts
(x: string) => console.log(x)

高階関数との組み合わせ↓

py
seq = [1, 2, 3, 4, 5]
print(list(filter(lambda x: x % 2 == 0, seq)))
> [2, 4]
ts
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)で行うというプロジェクトが多いみたいです(==nullundefinedを区別しないため)。またこれがTSで==演算子を使う唯一の機会でもあります。

nullにまつわる構文

最近のTSはかなりnull周りが書きやすくなっている印象があります。
以下の構文は両方ともPythonには存在しません。

ts
// オプショナルチェーン
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を駆逐するまでには至りません(頑張ってこねこねすればいけそうですが…)。

クラス

定義

py
from dataclasses import dataclass

@dataclass
class Player:
    name: str
    age: int

    def rename(self, name: str) -> None:
        self.name = name

me = Player("nicco", 29)
ts
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つの型システム「公称型」と「構造的部分型」

公称型に準じた設計とは

まさに上で書いたようなクラスがそれです。これを構造的部分型に準じた設計に変えてみましょう。

ts
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です。

解決策その2.ts
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 〜TypeScript始めたてのあの頃に知っておきたかったこと〜
カレンダー2日目の記事でした。
あの頃に知っておきたかったということでかなり基本に寄った内容でまとめてみましたがどうでしょうか?
特に環境周りは調べていて昔より随分楽になったんだな~と感じました。

また入門者の方はこれで一巡他の入門記事が読めるようになったのではないでしょうか。
おすすめは@uhyo氏のTypeScriptの型入門シリーズです。
本当にためになるシリーズなので未読の方はぜひ。


  1. グローバルに入れることもできますがnvmやらnpmを使っている意味が分からなくなるのでやめましょう 

  2. というかアレが珍しすぎますね 

  3. 正確に言えばletconstは、ですがvarは使わないので事実上 

  4. TSはクラスを定義すると同時に型も定義されます 

98
121
1

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
98
121