Upgrade to Pro — share decks privately, control downloads, hide ads and more …

TypeScript ⼩ネタとか

SansanTech
September 12, 2023

TypeScript ⼩ネタとか

■イベント
TypeScriptを活用した型安全なチーム開発
https://sansan.connpass.com/event/292695/

■登壇概要
タイトル:TypeScript ⼩ネタとか
登壇者:技術本部 Digitization部 ⼩⽥ 崇之

■Digitization部 エンジニア 採用情報
https://media.sansan-engineering.com/digitization

SansanTech

September 12, 2023
Tweet

More Decks by SansanTech

Other Decks in Technology

Transcript

  1. ⼩⽥ 崇之 Sansan株式会社 技術本部 Digitization部 Bill One Entry グループ 中途として2021年にSansanに⼊社。請求書のデータ化システム

    開発に従事。 今年から役割がマネジャーに変わり、かれこれ半年以上コードか ら離れているのに TypeScript の LT やりますとか⼿を挙げてしま い、100⼈以上集まったと聞いておっかなびっくりしています。
  2. アジェンダ - TypeScript でも Nominal Typing がしたい - 型推論に惑わされない; Promise

    と throw - 間話: git alias を晒してみる - NestJS でのモジュール設計
  3. const userRepository = { createUser(username: string, password: string) { return

    db.save({ username, password }); } } こんなコード書いた事ありませんか?
  4. こんな使われ⽅したら何がおきますか? const userService = { createUser(username: string, password: string) {

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

    const hashed = crypto.hash(password); return userRepository.createUser(username, password); } } 😱
  6. これはどうやったら検出できる? const orgRepository = { addMember(userId: string, organizationId: string) {

    return db.save({ userId, organizationId }); } } const orgService = { addMember(organizationId: string, userId: string) { orgRepository.addMember(organizationId, userId); } }
  7. 名前を与える型 (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]")
  8. type RawPassword = string; type HashedPassword = string; const password:

    RawPassword = "1qaz2wsx"; const hashed: HashedPassword = password; これでコンパイルエラーに!! TypeScript でもやってみる
  9. type RawPassword = string; type HashedPassword = string; 同じ構造なので全て同じ string

    型と⾒なされる type User = { name: string }; type Organization = { name: string }; 同じ構造なので全て同じ { name: string } 型と⾒なされる Structural Typing
  10. 構造ではなく名前に型が紐づく - Go, TypeScript 以外の⾔語で幅広く採⽤されている型の割当戦略 - Nominal → (形) 名前[名称]の[に関する]via

    英辞郎 - ⽇本語だと公称型 class Email extends String {} class Phone extends String {} 同じ String の拡張だけど違う型
  11. プリミティブ型と存在しないプロパティを交差型で定義する事で、 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 というテクニック
  12. ジェネリクスを使ったりして整理するとこんな感じにできる declare const __brand: unique symbol type Brand<B> = {

    [__brand]: B }; export type Branded<T, B> = T & Brand<B>; type Raw = Branded<string, "Raw">; 参考⽂献: Improve Runtime Type Safety with Branded Types in TypeScript | egghead.io Branded Type というテクニック
  13. type DateRange = { since: Date, until: Date }; type

    doUpdate = (id: number, data: DateRange)=> Promise<boolean>; 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 関数の返り値、型推論の結果は?
  14. Jest で書いてます。 describe('update 関数', () => { it('until が since

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

    async () => { const since = new Date(); const until = new Date(0); await expect(update(1, since, until).rejects.toThrow() }); });
  16. type DateRange = { since: Date, until: Date }; type

    doUpdate = (id: number, data: DateRange)=> Promise<boolean>; 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 関数の返り値、型推論の結果は?
  17. この Jest で求めていた型は describe('update 関数', () => { it('until が

    since よりも⼩さい場合に例外を投げる', async () => { const since = new Date(); const until = new Date(0); await expect(update(1, since, until).rejects.toThrow() }); });
  18. type DateRange = { since: Date, until: Date }; type

    doUpdate = (id: number, data: DateRange)=> Promise<boolean>; 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 を返す
  19. type DateRange = { since: Date, until: Date }; type

    doUpdate = (id: number, data: DateRange)=> Promise<boolean>; 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 キーワードをつける
  20. 他のアプローチ - Go の様にエラー返す - Haskell の Either 型や Rust

    の Result 型みたいにラップして返す 結局は「エラーが起きうることを明⽰する」という⼿法 Java の throws もそういう意味ではエラーの可視化がされていたなと
  21. - 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
  22. 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>' -- @ ※ ⾊関連のコマンドはスペースの都合上省略しました
  23. @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 {}
  24. モジュール設計を考えるいい機会 ここは 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 {}
  25. UserModule import: [AuthModule] provider: [UserService, UserRepository] exports: [UserService] モジュールが相互依存するシーン AuthModule

    import: [UserModule] provider: [AuthService] exports: [UserService] ユーザーのプロフィール編集するために、認証認可を確認したい ユーザーの認証認可をするために、ユーザー情報を取得したい
  26. - 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年の⾃分 ツールとか参考⽂献