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

TypeScript 関数型スタイルでバックエンド開発のリアル

TypeScript 関数型スタイルでバックエンド開発のリアル

TSKaigi 2024 のスライドです

Avatar for Naoya Ito

Naoya Ito

May 10, 2024
Tweet

More Decks by Naoya Ito

Other Decks in Programming

Transcript

  1. ͳ͓ɺॻ੶ͦͷ௨Γʹ͸΍͍ͬͯͳ͍ • υϝΠϯΦϒδΣΫτΛܕͰදݱ͢Δ ˠ͍͍Ͷ • ΤϥʔʹΑΔ෼ذ͸ 3FTVMUܕͰѻ͍ɺΤϥʔॲཧΛ໌ࣔతʹ͢Δ ˠ ͍͍Ͷ •

    XPSLGMPXʹ௚઀υϝΠϯΦϒδΣΫτΛఆٛ͢Δ τϥϯβΫγϣϯεΫϦϓτ  3FQPTJUPSZύλʔϯ͸ཁΒͳ͍ ˠͳͥ 🤔 ࢀߟʹ͍ͯ͠ͳ͍
  2. 5ZQF4DSJQUͰɺͲΜͳελΠϧͰ࣮૷͍ͯ͠Δ͔ • υϝΠϯΦϒδΣΫτ͸ܕͰߏ଄Λఆٛ͢Δ – DMBTTͰ͸ͳ͘ JOUFSGBDF • ΦϒδΣΫτͷมߋ͸ʮؔ਺ద༻ʹΑΔঢ়ଶભҠʯͱͯ͠Πϛϡʔλϒϧʹ͢Δ – ʮΦϒδΣΫτͷ಺෦ঢ়ଶͷॻ͖׵໋͑ྩʯͰ͸ͳ͘ʮؔ਺ద༻

    ࣸ૾ ʹΑΓঢ়ଶΛҠ͢ʯ • ۩ମతͳϢʔεέʔε͸ XPSLGMPXͱ࣮ͯ͠૷ – ೖྗ஋ͱυϝΠϯΦϒδΣΫτ͕υϝΠϯΠϕϯτʹ൐͍৽͍͠ঢ়ଶʹભҠɻͦͷաఔΛܕͰఆٛ – ঢ়ଶભҠʹ͸ࣦഊ Τϥʔ ͕൐͏ɻ໭Γೳ͍Λ 3FTVMUʹ͠ʮ3FTVMUΛฦؔ͢਺ͷ߹੒ʯͰϑϩʔΛߏ੒ – 3FTVMU͸ඪ४ʹͳ͍ͷͰ OFWFSUISPXΛར༻ ಥ͖٧ΊΔͱɺ͕ؔ͜͜ ਺ܕϓϩάϥϛϯά
  3. υϝΠϯΦϒδΣΫτ͸ܕͰఆٛ͢Δ export interface Customer { id: CustomerId groupId: RestaurantGroupId name:

    CustomerName nameKana: CustomerNameKana | null nickname: CustomerNickname | null gender: Gender smoking: SmokingType memo: CustomerMemo | null archived: boolean phoneNumbers: CustomerPhoneNumber[] workplaces: CustomerWorkplace[] restaurantContexts: CustomerRestaurantContext[] emails: CustomerEmail[] events: CustomerEvent[] tagIds: TagId[] allergyIds: AllergyId[] }
  4. 6OJPOܕΛੵۃతʹར༻͢Δ export type Reservation = DirectReservation | SiteReservation interface _Reservation

    { id: ReservationId restaurantId: RestaurantId type: ReservationType events: ReservationEvent[] customerIds: CustomerId[] confirmStatus: Confirmed | Unconfirmed operationStatus: BuiltInOperationStatus | CustomOperationStatus orders: Order[] } export interface DirectReservation extends _Reservation { type: 'Direct' events: DirectReservationEvent[] } export interface SiteReservation extends _Reservation { type: 'Site' events: ReservationEvent[] }
  5. ஋ΦϒδΣΫτ // Nominal な型 export type CustomerId = newtype<'CustomerId', string>

    declare const __newtype: unique symbol; export type newtype<Constructor, Type> = Type & { readonly [__newtype]: Constructor; };
  6. ஋ΦϒδΣΫτ͸ίϯετϥΫλͰੜ੒ ࣦഊ͸ 3FTVMUͰฦ͢ export type Color = newtype<'Color', string> export

    function Color(value: string): Result<Color, ValidationError> { return /^#[0-9a-f]{3}([0-9a-f]{3})?$/i.test(value) ? ok(value as Color) : err(new ValidationError('色の値が不正です。#FFFFFF形式で指定してください')) }
  7. customer.archive() const archived = archiveCustomer(customer) • ໋ྩతʹॻ͘ – ΦϒδΣΫτͷ಺෦ঢ়ଶΛॻ͖׵͑Δ໋ྩΛߦ͏͜ͱͰɺঢ়ଶΛมԽͤ͞Δ –

    ঢ়ଶͷมԽ͕҉໧త uuu໭Γ஋͕ͳ͍ • ؔ਺తʹॻ͘ – Ҿ਺ͷΦϒδΣΫτʹؔ਺Λద༻ͯ͠ɺঢ়ଶભҠޙͷΦϒδΣΫτΛಘΔ – ঢ়ଶͷมԽ͕໌ࣔత uuu ભҠલͷঢ়ଶ͸ඞͣҾ਺ʹݱΕɺભҠޙͷঢ়ଶ͸໭Γ஋ʹݱΕΔ
  8. ϫʔΫϑϩʔͷͱ͋Δαϒεςοϓ uuu ࣦഊͱͷ෼ذ͸ 3FTVMUͰදݱ // 予約リクエストから来店者情報を抽出 type extractActualVisitor = (command:

    SiteReservationValidated) => Result<VisitorExtracted, ValidationError> const extractActualVisitor: extractActualVisitor = (command) => ok(command.input.reservationEvent.siteReservation.guest) .andThen(ReservationGuest) .map(choiceActualVisitor) .map((visitor) => ({ ...command, kind: 'VisitorExtracted' as const, visitor: visitor, }))
  9. ϫʔΫϑϩʔʹ͓͚Δঢ়ଶભҠΛܕͰఆٛ͢Δ interface UnvalidatedCommand { kind: 'Unvalidated' input: { groupId: RestaurantGroupId

    reservationEvent: UnvalidatedSiteReservationEvent } } interface SiteReservationValidated { kind: 'SiteReservationValidated' input: { groupId: RestaurantGroupId reservationEvent: ValidatedSiteReservationEvent } } interface VisitorExtracted { kind: 'VisitorExtracted' input: { groupId: RestaurantGroupId reservationEvent: ValidatedSiteReservationEvent } visitor: Visitor | ReservationHolder } interface CustomerNotIdentified { kind: 'CustomerNotIdentified' input: { groupId: RestaurantGroupId visitor: Visitor | ReservationHolder customer: ValidatedCustomer reservationEvent: ValidatedSiteReservationEvent } } interface CustomerIdentified { kind: 'CustomerIdentified' input: { groupId: RestaurantGroupId visitor: Visitor | ReservationHolder customer: IdentifiedCustomer reservationEvent: ValidatedSiteReservationEvent } } ... interface SiteReservationEventImported { kind: 'SiteReservationEventImported' reservation: CreatedReservation customer: CreatedCustomer | UnchangedCustomer | UpdatedCustomer }
  10. Ұ࿈ͷαϒεςοϓΛ߹੒͠ϫʔΫϑϩʔ Ϣʔεέʔε Λߏ੒͢Δ type WorkFlow = (command: UnvalidatedCommand) => ResultAsync<ImportSiteReservationEvent[],

    ImportSiteReservationError> export const importReservationEventWorkFlow = ( findIdenticalCustomer: findIdenticalCustomer, // ドメンサービスを高階関数としてDI findReservationSlotsByTableAllocations: findReservationSlotsByTableAllocations, findBestTablesForAssignedTable: findBestTablesForAssignedTable ): WorkFlow => (command) => ok(command) .andThen(validateReservationEvent) // バリデーション .andThen(extractActualVisitor) // 来店者情報の抽出 .asyncAndThen(identifyCustomer(findIdenticalCustomer)) // 名寄せ .andThen((command) => { switch (command.kind) { case 'CustomerIdentified': return updateCustomer(command) // 顧客の更新 case 'CustomerNotIdentified': return createNewCustomer(command) // 新規顧客の作成 } }) .andThen(importReservationEvent(findBestTablesForAssignedTable, isOverlappingWithOtherReservations) .andThen(findAffectedReservationSlots(findReservationSlotsByTableAllocations)
  11. ϫʔΫϑϩʔΛ (SBQI2-NVUBUJPO͔Βݺͼ *0ͰαϯυΠον͢Δ builder.mutationField('updateTablePattern', (t) => t.field({ type: UpdateTablePatternPayload, args:

    { patternId: t.arg({ type: 'UUID', description: 'テーブルパターンID' }), input: t.arg({ type: TablePatternInput }), }, authScopes: (_parent, { patternId }, context) => canUpdateTablePattern(context, patternId), resolve: (_parent, { patternId, input }, context) => { // ドメインサービスを DI してワークフローを作る const workflow = updateTablePatternWorkFlow(checkTablePatternExists(context)) const preprocess = ok(patternId) .andThen(TablePatternId) .asyncAndThen(getTablePatternById(context)) .map((pattern) => toUnvalidatedCommand({ input, pattern })) const result = preprocess.andThen(workflow).andThen(updateTablePattern(context)) return result.match( (pattern) => ({ pattern }), (error) => { throw error }, ) }, }), ) σʔλऔಘ σʔλอଘ ߋ৽
  12. σʔλϕʔε *0͸ 3FQPTJUPSZύλʔϯ ͨͩͨͩ͠ͷؔ਺  export const findTagById = ({

    prisma }: applicationContext) => (id: TagId): ResultAsync<Tag | null, ValidationError | PrismaClientError> => ResultAsync.fromPromise( prisma.tag.findUnique({ where: { id } }), PrismaClientError, ).andThen((tag) => (tag ? Tag(tag) : ok(null))) export const getTagById = (context: applicationContext) => (id: TagId): ResultAsync<Tag, EntityNotFound | ValidationError | PrismaClientError> => findTagById(context)(id).andThen((tag) => tag ? ok(tag) : err(new EntityNotFound(`タグがみつかりません: ${id}`)), ) export const createTag = ({ prisma }: applicationContext) => (model: CreatedTag): ResultAsync<TagData, PrismaClientError> => { const { kind: _, ...tag } = model const icon = toIconData(tag.icon) return ResultAsync.fromPromise( prisma.tag.create({ data: { ...tag, ...icon, }, }), PrismaClientError, ) }
  13. *0ˠυϝΠϯ૚ υϝΠϯΦϒδΣΫτͷঢ়ଶભҠ ˠ*0 • Ϣʔβʔ͔Βͷೖྗɺσʔλϕʔε͔ΒͷΦϒδΣΫτͷऔಘɺอଘΛ੍ޚ͢Δͷ͸ (SBQI2- NVUBUJPO • σʔλϕʔε͔ΒͷΦϒδΣΫτͷऔಘɺอଘ͸ 3FQPTJUPSZuuu

    ͕͜͜ *0ڥք • ೖྗͱυϝΠϯΦϒδΣΫτΛʮίϚϯυΦϒδΣΫτʯʹ·ͱΊͯʮϫʔΫϑϩʔʯʹྲྀ͠ࠐΉɻ͜ ͔͜Βઌ͕ɺυϝΠϯϨΠϠʔ • ϫʔΫϑϩʔ͔Βग़͖ͯͨυϝΠϯΠϕϯτ యܕతʹ͸ঢ়ଶભҠࡁΈͷυϝΠϯΦϒδΣΫτͷอଘ  Λݩʹग़ྗͷ *0Λ࣮ߦ
  14. γεςϜશମͷ࣮૷͸ɺैདྷͷ࣮૷͔ΒͦΕ΄ͲมΘΒͳ͍ • ΞϓϦέʔγϣϯશମ͸͜Ε·Ͱ௨ΓͷΦχΦϯΞʔΩςΫνϟʔ • υϝΠϯϨΠϠʔ͸ؔ਺ܕͱ͍ͬͯ΋ϙΠϯτ͸ओʹͭͷΈ – ΦϒδΣΫτͷߏ଄Λܕ JOUFSGBDF Ͱදݱ͢Δ –

    ΦϒδΣΫτͷมߋΛɺؔ਺ద༻ ঢ়ଶભҠͰΠϛϡʔλϒϧ໌ࣔతʹදݱ͢Δ – ࣦഊͷ෼ذ Τϥʔ ͸ 3FTVMUͰදݱ͢Δ • ΦϒδΣΫτࢦ޲Ͱ΍͍ͬͯͨͱ͖ͱɺγεςϜΛ੔ཧ͢Δߟ͑ํมΘͬͯͳ͍ – υϝΠϯϞσϦϯάΛͯ͠ɺू໿ɺΤϯςΟςΟɺ஋ΦϒδΣΫτʹ੔ཧ͠ߏ଄Խ͢Δ – ΦϒδΣΫτͷܕఆٛͱಉ͡ϑΝΠϧɺۙ͘ʹͦͷΦϒδΣΫτʹద༻͢Δؔ਺͕͋Δ
  15. ͳͥυϝΠϯϨΠϠʔΛؔ਺ܕελΠϧʹ͍ͨ͠ͷ͔ • ܕΛ༗ޮʹར༻͍ͨ͠ – ੩తݕࠪʹد͍ͤͯ͘ͱʮಈ͔͞ͳ͍ͱ෼͔Βͳ͍͜ͱʯ͕ݮΔ – υϝΠϯ૚ʹ͓͍ͯʮෆඞཁͳঢ়ଶଘࡏʯΛݮΒ͢ͱݎ࿚ʹͳΔ uuu 6OJPOܕΛੵۃར༻͍ͨ͠ •

    ΦϒδΣΫτͷมߋΛʮؔ਺ద༻ʹΑΔঢ়ଶભҠʯʹ͢ΔͱܕΛهड़͠΍͍͢ – ঢ়ଶભҠલɺޙͷΦϒδΣΫτ͕ؔ਺ͷҾ਺ͱ໭Γ஋ʹදΕΔ • ࣦഊʹΑΔ෼ذ uuu ͭ·ΓΤϥʔॲཧ΋ܕͰѻ͍͍ͨɻ3FTVMUܕ – Τϥʔέʔε͕ᐆດʹͳΔͷ͸ɺۀ຿γεςϜͰ͸ා͍ ͭ·Γɺ໨త͸ʮؔ਺ܕϓϩάϥϛϯάΛ͢Δ͜ͱʯͰ͸ͳ͘ ʮܕɺ੩తݕࠪΛΑΓੵۃతʹར༻͍ͨ͠ʯͱ͜Ζʹ͋Γ·͢
  16. ؔ਺ܕελΠϧΛར༻͍ͨͨ͠Ίͷɺ*0෼཭ uuu ΦχΦϯΞʔΩςΫνϟ • *0ґଘ͕͋Δͱ͔ͦ͜ΒҶͮΔࣜతʹखଓ͖త ໋ྩత ʹͳΓ΍͍͢ • υϝΠϯϨΠϠʔΛ *0ͱ෼཭͢Δ͜ͱͰɺυϝΠϯϨΠϠʔΛؔ਺ܕελΠϧʹ͢Δ༨஍͕ੜ·ΕΔ

    • ͦͷͨΊͷΦχΦϯΞʔΩςΫνϟ – υϝΠϯϩδοΫͷ్தͰ *0͕͋Δ৔߹͸ߴ֊ؔ਺ʹΑΔ %*Λߦ͍ɺϫʔΫϑϩʔࣗମ͸७ਮؔ਺Λҡ࣋͢Δ • ݁ՌɺυϝΠϯΦϒδΣΫτ͸෭࡞༻Λ൐Θͳ͍७ਮͳΦϒδΣΫτ΍ؔ਺ͱͳΓɺܕͰͷදݱ͕༰қ ʹͳΔ
  17. ໘౗ͳͱ͜Ζ uuu 3FTVMUܕύζϧ export const Tag = (input: TagInput): Result<Tag,

    ValidationError> => { const tagId = TagId(input.id) const groupId = RestaurantGroupId(input.groupId) const label = TagLabel(input.label) const icon = input.icon && input.iconType ? TagIcon({ symbol: input.icon, type: input.iconType, color: input.color, }) : ok(NoIcon()) const sortOrder = FractionalIndex(input.sortOrder) const values = Result.combine(tuple(tagId, groupId, label, icon, sortOrder)) return values.map(([id, groupId, label, icon, sortOrder]) => ({ ...input, id, groupId, label, icon, sortOrder, })) } ஋͕͍͍ͩͨ3FTVMUʹೖ͍ͬͯΔ ͨΊɺෳ਺3FTVMU͕͋Δͱ߹੒͠ ͯϑϥοτʹ͢Δඞཁ͕͋Γ໘౗ ͳ࡞ۀʹͳͬͯ͘Δ ݎ࿚Ͱ͸͋Δ
  18. ʮίϯςφʹೖͬͨจ຺͖ͭͷ஋ʯΛѻ͏ɺ૊ΈࠐΈͷػೳ͕͋ΔݴޠͳΒuuu ͜ͷखͷػߏ͕͋Ε͹ೝ஌ෛՙ௿࣮͘૷Ͱ͖Δ͕ɺ࢒೦ͳ͕Β 5ZQF4DSJQUʹ͸ͳ͍ makeTagId :: String -> Either ValidationError TagId

    makeTagId tagId | null tagId = Left (ValidationError "tagId is empty") | otherwise = Right (TagId tagId) makeTag :: String -> String -> Int -> Either ValidationError Tag makeTag id gid order = do tagId <- makeTagId id groupId <- makeGroupId gid sortOrder <- makeSortOder order return (Tag tagId groupId sortOrder) )BTLFMM ྫ͑͹Ϟφυʹ͸ɺೖΕࢠʹ ͳͬͨίʔυΛฏୱԽ͢Δޮ༻ ͕͋Δ
  19. ໋ྩͱ 3FTVMU const result = ok(command).asyncAndThen(({ date, restaurantIds }) =>

    { const results = [] for (const restaurantId of restaurantIds) { const task = { restaurantId, date, } const result = ok(task) .asyncAndThen(sender) .mapErr((error) => ({ restaurantId, error })) results.push(result) } return ResultAsync.combineWithAllErrors(results) }) ໋ྩతͳ࣮૷Ͱෳ਺ͷ 3FTVMU ͕བྷΜͰ͘Δͱɺύζϧײ
  20. ໋ྩతʹॻ͍ͨํ͕͍͍ॴ͸खଓ͖Ͱॻ͖ GSPN5ISPXBCMFGSPN1SPNJTFͰแΉ export const sendTask = <T>({ project, queue, location,

    url, serviceAccountEmail }: CloudTasksClientConfig) => (body: T) => { const parent = client.queuePath(project, location, queue) const task: google.cloud.tasks.v2.ITask = { httpRequest: { headers: { 'Content-Type': 'application/json', }, httpMethod: 'POST' as const, url, body: Buffer.from(JSON.stringify(body)).toString('base64'), oidcToken: { serviceAccountEmail, }, }, } return fromPromise( client.createTask({ parent, task }), (error) => new NetworkError(error as string, { cause: error }), ) } ίϯϐϡʔλ΁ͷ໋ྩͳͷ͔ͩ Βɺૉ௚ʹखଓ͖Ͱॻ͚͹ྑ͍
  21. ΍ͬͯΈͯɺ࣮ࡍͲ͏ • ैདྷͷ։ൃΑΓ΋ɺݎ࿚ʹͳͬͨͱࢥ͏ – ӡ༻ظؒ ೥΄Ͳɻෆ۩߹ʹΑΔো֐͕গͳ͍ɻίʔυͷܦ೥ྼԽ΋཈͑ΒΕ͍ͯΔ – ୹ظతͳ։ൃεϐʔυʹ͸ྑ͘΋ѱ͘΋Өڹ͸ͳ͍ ଎͘ͳͬͨɺͱ͔͸ͳ͍ 3FTVMUύζϧ͸໘౗

    • Τϥʔͷॲཧ࿙Ε ૝ఆ͍ͯ͠ͳ͍ঢ়ଶʹΑΔෆ۩߹͕গͳ͍ɻܕ΍ 3FTVMUͰݻΊ͍ͯΔޮ༻ – ӡ༻͍ͯͯͦ͠͏͍͏όά͕ग़Δ͜ͱ͸رɻόά͕ग़Δͷ͸ཁ݅ఆٛࣗମͷؒҧ͍΍ɺ࢓༷ͷߟྀ࿙Ε • Ϣχοτςετ͸ैདྷ։ൃΑΓݮͬͨ – ܕͰอূͰ͖ΔྖҬ͕֦͕ͬͨ͜ͱʹΑΓςετͰͷอূ͕ෆཁʹͳΔ͜ͱ͸ଟ͍ – ΋ͪΖΜ͢΂ͯΛܕͰอূͰ͖ͨΓ͠ͳ͍ͷͰɺϢχοτςετ͸ॻ͘ • ΦϯϘʔσΟϯάେม – υϝΠϯϨΠϠʔͷ࣮૷ʹؔͯ͠ΑΓஸೡͳΦϯϘʔσΟϯά͕ඞཁɻ৽͍͠։ൃऀ͕ࢀը͙ͯ͢͠͸ίʔυϨϏϡʔΛްΊʹͯ͠ ͍Δ – ࠷ॳ͸ 3FTVMUܕΛෳࡶʹ͕ͪ͠ 😂
  22. ͝ࢀߟ uuu ҎલͷൃදεϥΠυͳͲ • 5ZQF4DSJQUʹΑΔ (SBQI2-όοΫΤϯυ։ൃ  4QFBLFS%FDL – IUUQTTQFBLFSEFDLDPNOBPZBUZQFTDSJQUOJZPSVHSBQIRMCBUVLVFOEPLBJGBCEBCBBED

    EFCECE • ؔ਺ܕϓϩάϥϛϯάͱܕγεςϜͷϝϯλϧϞσϧ r 4QFBLFS%FDL – IUUQTTQFBLFSEFDLDPNOBPZBHVBOTIVYJOHQVSPHVSBNJOHVUPYJOHTJTVUFNVOPNFOUBSVNPEFSV • 5ZQF4DSJQUͰͲ͜·Ͱʮؔ਺ܕϓϩάϥϛϯάʯ͢Δ͔ ᴷʮखଓ͖ )BTLFMMʯ͔Βߟ࡯͢Δ  ҰٳDPN %FWFMPQFST#MPH – IUUQTVTFSGJSTUJLZVDPKQFOUSZ