Slide 1

Slide 1 text

Sansan株式会社 部署 名前 TypeScript ⼩ネタとか Sansan技術本部 Sansan技術本部 技術本部 Digitization部 ⼩⽥ 崇之

Slide 2

Slide 2 text

⼩⽥ 崇之 Sansan株式会社 技術本部 Digitization部 Bill One Entry グループ 中途として2021年にSansanに⼊社。請求書のデータ化システム 開発に従事。 今年から役割がマネジャーに変わり、かれこれ半年以上コードか ら離れているのに TypeScript の LT やりますとか⼿を挙げてしま い、100⼈以上集まったと聞いておっかなびっくりしています。

Slide 3

Slide 3 text

アジェンダ - TypeScript でも Nominal Typing がしたい - 型推論に惑わされない; Promise と throw - 間話: git alias を晒してみる - NestJS でのモジュール設計

Slide 4

Slide 4 text

TypeScript でも Nominal Typing がしたい

Slide 5

Slide 5 text

const userRepository = { createUser(username: string, password: string) { return db.save({ username, password }); } } こんなコード書いた事ありませんか?

Slide 6

Slide 6 text

こんな使われ⽅したら何がおきますか? const userService = { createUser(username: string, password: string) { const hashed = crypto.hash(password); return userRepository.createUser(username, password); } }

Slide 7

Slide 7 text

こんな使われ⽅したら何がおきますか? const userService = { createUser(username: string, password: string) { const hashed = crypto.hash(password); return userRepository.createUser(username, password); } } 😱

Slide 8

Slide 8 text

これはまぁそもそもコンパイラやLintで検出すべきだけど

Slide 9

Slide 9 text

これはどうやったら検出できる? const orgRepository = { addMember(userId: string, organizationId: string) { return db.save({ userId, organizationId }); } } const orgService = { addMember(organizationId: string, userId: string) { orgRepository.addMember(organizationId, userId); } }

Slide 10

Slide 10 text

このミスをコンパイラで検知できるとしたら?

Slide 11

Slide 11 text

名前を与える型 (Opaque Types) プリミティブな型をクラスでラップして「別の型」として扱う - Elm > type Email = Email String - Scala > opaque type Email = String - Kotlin > value class Email(val email: String) value class Email(val email: String) // false Email("[email protected]") == "[email protected]" // true Email("[email protected]") == Email("[email protected]")

Slide 12

Slide 12 text

type RawPassword = string; type HashedPassword = string; const password: RawPassword = "1qaz2wsx"; const hashed: HashedPassword = password; これでコンパイルエラーに!! TypeScript でもやってみる

Slide 13

Slide 13 text

ならない

Slide 14

Slide 14 text

TypeScript が採⽤しているのはStructural Typing - Go⾔語でも採⽤されている型の割当戦略 - Structural → (形) 構造物の、構造(上)の via 英辞郎 - ⽇本語だと構造的部分型 - その名の通り「構造」に対して型がつく

Slide 15

Slide 15 text

type RawPassword = string; type HashedPassword = string; 同じ構造なので全て同じ string 型と⾒なされる type User = { name: string }; type Organization = { name: string }; 同じ構造なので全て同じ { name: string } 型と⾒なされる Structural Typing

Slide 16

Slide 16 text

構造ではなく名前に型が紐づく - Go, TypeScript 以外の⾔語で幅広く採⽤されている型の割当戦略 - Nominal → (形) 名前[名称]の[に関する]via 英辞郎 - ⽇本語だと公称型 class Email extends String {} class Phone extends String {} 同じ String の拡張だけど違う型

Slide 17

Slide 17 text

TypeScript では Nominal Typing できない?

Slide 18

Slide 18 text

プリミティブ型と存在しないプロパティを交差型で定義する事で、 string を代⼊しようと すると型的にエラーになる⼀⽅、 string として扱える型が作れる。 type Raw = string & { __brand: "Raw" }; const raw: Raw = "password"; // Error // Type 'string' is not assignable to type '{ __brand: "Raw"; }' const raw: Raw = "password" as Raw; const identity = (text: string) => text; identity(rawPassword); Branded Type というテクニック

Slide 19

Slide 19 text

ジェネリクスを使ったりして整理するとこんな感じにできる declare const __brand: unique symbol type Brand = { [__brand]: B }; export type Branded = T & Brand; type Raw = Branded; 参考⽂献: Improve Runtime Type Safety with Branded Types in TypeScript | egghead.io Branded Type というテクニック

Slide 20

Slide 20 text

名前もいいけど、型でも頑張る

Slide 21

Slide 21 text

型推論に惑わされない; Promise と throw

Slide 22

Slide 22 text

- await キーワードを使わなくても使う - await キーワードを使わないなら使わない async キーワードは...

Slide 23

Slide 23 text

早速問題です

Slide 24

Slide 24 text

type DateRange = { since: Date, until: Date }; type doUpdate = (id: number, data: DateRange)=> Promise; const update = (id: number, since: Date, until: Date) => { if (since.getTime() > until.getTime()) { throw new Error('Since cannot be before until'); } return doUpdate(id, { since, until, }); }; この update 関数の返り値、型推論の結果は?

Slide 25

Slide 25 text

Promise

Slide 26

Slide 26 text

はい、Promise になります。 const update: (id: number, since: Date, until: Date)=> Promise

Slide 27

Slide 27 text

テストを書いてみる

Slide 28

Slide 28 text

Jest で書いてます。 describe('update 関数', () => { it('until が since よりも小さい場合に例外を投げる', async () => { const since = new Date(); const until = new Date(0); await expect(update(1, since, until).rejects.toThrow() }); });

Slide 29

Slide 29 text

このテストは例外を吐いて死にます

Slide 30

Slide 30 text

例外が出ること期待してるのに?例外吐いてエラー??? describe('update 関数', () => { it('until が since よりも小さい場合に例外を投げる', async () => { const since = new Date(); const until = new Date(0); await expect(update(1, since, until).rejects.toThrow() }); });

Slide 31

Slide 31 text

何が起きているのか?

Slide 32

Slide 32 text

type DateRange = { since: Date, until: Date }; type doUpdate = (id: number, data: DateRange)=> Promise; const update = (id: number, since: Date, until: Date) => { if (since.getTime() > until.getTime()) { return new Error('Since cannot be before until'); } return doUpdate(id, { since, until, }); }; この update 関数の返り値、型推論の結果は?

Slide 33

Slide 33 text

Promise | Error;

Slide 34

Slide 34 text

Error が Promise の 外 にある

Slide 35

Slide 35 text

この Jest で求めていた型は describe('update 関数', () => { it('until が since よりも⼩さい場合に例外を投げる', async () => { const since = new Date(); const until = new Date(0); await expect(update(1, since, until).rejects.toThrow() }); });

Slide 36

Slide 36 text

Promise; ☢ 正しくないよ!ニュアンスだよ!!

Slide 37

Slide 37 text

type DateRange = { since: Date, until: Date }; type doUpdate = (id: number, data: DateRange)=> Promise; const update = (id: number, since: Date, until: Date) => { if (since.getTime() > until.getTime()) { return Promise.reject(new Error('Since cannot be before until')); } return doUpdate(id, { since, until, }); }; どう書くべきか? Promise を返す

Slide 38

Slide 38 text

type DateRange = { since: Date, until: Date }; type doUpdate = (id: number, data: DateRange)=> Promise; const update = async (id: number, since: Date, until: Date) => { if (since.getTime() > until.getTime()) { return Promise.reject(new Error('Since cannot be before until')); } return doUpdate(id, { since, until, }); }; どう書くべきか? async キーワードをつける

Slide 39

Slide 39 text

例外は型をすり抜ける から気をつけようねというお話しでした。

Slide 40

Slide 40 text

他のアプローチ - Go の様にエラー返す - Haskell の Either 型や Rust の Result 型みたいにラップして返す 結局は「エラーが起きうることを明⽰する」という⼿法 Java の throws もそういう意味ではエラーの可視化がされていたなと

Slide 41

Slide 41 text

閑話休題: git alias の話

Slide 42

Slide 42 text

- alias.me !git config --get-regexp user - alias.aliass !git config --get-regexp alias - alias.amend commit --amend - alias.wip commit --no-verify -m WIP - alias.cancel reset --mixed HEAD^ - alias.forcepush push --force-with-lease - alias.purr pull --rebase - alias.nerge merge --no-ff - alias.ibase !sh -c 'git rebase -i --autosquash $(git merge-base ${1:-main} HEAD)' - - alias.l log --oneline - alias.lm log --oneline origin/main.. - alias.lp log --abbrev-commit --color --pretty=format:'%C(yellow)%h%Creset - %C(auto)%d%Creset %s %C(blue)(%cd)%C(red)<%an>%Creset' --date iso - alias.ls log --decorate --oneline --stat - alias.noskip update-index --no-skip-worktree - alias.plune !git branch --merged | grep -vE '(main|master|develop|release|gh-pages|¥*)' | xargs git branch -d - alias.pwd rev-parse --show-prefix - alias.skip update-index --skip-worktree - alias.stat !cd -- ${GIT_PREFIX:-.} && git stash list && git status --short --branch - alias.up !git branch -u origin/$(git branch --show-current) - alias.wdiff diff --ignore-all-space --word-diff

Slide 43

Slide 43 text

alias.files !cd ${GIT_PREFIX:-.} && git ls-tree -z --name- only HEAD | xargs -0 -n1 -I@ -- git log -1 -- pretty=format:'%ai %h @ (%ar) <%an>' -- @ ※ ⾊関連のコマンドはスペースの都合上省略しました

Slide 44

Slide 44 text

NestJS でのモジュール設計

Slide 45

Slide 45 text

という名の、こういう時ってどうするんですかね?話

Slide 46

Slide 46 text

@Module({ imports: [EventModule] }) export class ApplicationModule {} NestJSいいですよね。 @Module({ controllers: [UserController], providers: [UserService, UserRepository], exports: [UserService], }) export class UserModule {} @Module({ imports: [UserModule], controllers: [EventController], providers: [EventService], exports: [EventService], }) export class EventModule {}

Slide 47

Slide 47 text

モジュール設計を考えるいい機会 ここは User ではなく Participant かも? 純粋なUser?それとも ログインしたUser? @Module({ imports: [EventModule] }) export class ApplicationModule {} @Module({ controllers: [UserController], providers: [UserService, UserRepository], exports: [UserService], }) export class UserModule {} @Module({ imports: [UserModule], controllers: [EventController], providers: [EventService], exports: [EventService], }) export class EventModule {}

Slide 48

Slide 48 text

UserModule と AuthModule

Slide 49

Slide 49 text

UserModule import: [AuthModule] provider: [UserService, UserRepository] exports: [UserService] モジュールが相互依存するシーン AuthModule import: [UserModule] provider: [AuthService] exports: [UserService] ユーザーのプロフィール編集するために、認証認可を確認したい ユーザーの認証認可をするために、ユーザー情報を取得したい

Slide 50

Slide 50 text

どう循環を剥がすか?

Slide 51

Slide 51 text

UserModule provider: [UserRepository] export: [UserService] 2つのモジュールに依存するモジュールを作る AuthModule provider: [UserRepository] export: [AuthService] AuthUserModule import: [UserModule, AuthModule]

Slide 52

Slide 52 text

2つのモジュールに依存するモジュールを作る UserModule provider: [UserRepository] export: [UserService] AuthModule provider: [UserRepository] export: [AuthService] AuthUserModule import: [UserModule, AuthModule]

Slide 53

Slide 53 text

結局欲しいのは UserRepository へのアクセス?

Slide 54

Slide 54 text

UserModule provider: [UserRepository] export: [UserService] Repository を Module と分離させた AuthModule provider: [UserRepository] export: [AuthService] UserRepository

Slide 55

Slide 55 text

- SlidesCodeHighlighter - https://romannurik.github.io/SlidesCodeHighlighter - スライドに貼るためのコードをいい感じに⽣成してくれる - Improve Runtime Type Safety with Branded Types in TypeScript - https://egghead.io/blog/using-branded-types-in-typescript - Branded Type についてのいい感じの記事 - NestJS - https://docs.nestjs.com/ - お世話になりました - 元ネタ - TypeScript でも Nominal Typing がしたい > https://qiita.com/takayukioda/items/32de7f05b7dd9e025ce7 > ありがとう2019年の⾃分 - Promiseを返す関数で throw する時の注意点 > https://qiita.com/takayukioda/items/df0e0320d803bf28ba22 > ありがとう2020年の⾃分 ツールとか参考⽂献

Slide 56

Slide 56 text

Sansan 技術本部 Digitization部 採⽤情報 https://media.sansan-engineering.com/digitization

Slide 57

Slide 57 text

No content