TypeScript

Page content

TypeScriptは、ざっくり言うと
👉 JavaScriptに「型(type)」を足して安全にした言語です。

「文法」を最短で使えるレベルに絞って解説します。


① 基本の型(ここが出発点)

let name: string = "Seiichi";
let age: number = 30;
let isActive: boolean = true;

👉 型を書くだけで

  • バグ減る

  • 補完効く


② 配列・オブジェクト

let numbers: number[] = [1, 2, 3];

let user: { id: string; age: number } = {
  id: "u1",
  age: 25,
};

③ 関数

function add(a: number, b: number): number {
  return a + b;
}

👉 : number は戻り値の型


④ 型エイリアス(実務で超使う)

type User = {
  id: string;
  email: string;
  isActive: boolean;
};

const user: User = {
  id: "1",
  email: "test@test.com",
  isActive: true,
};

👉 長い型は type でまとめる


⑤ Optional(任意プロパティ)

type User = {
  id: string;
  email?: string;
};

👉 ? があると無くてもOK


⑥ Union型(どっちか)

let status: "active" | "trialing" | "canceled";

👉 Stripe系ではこれ必須


⑦ 型推論(書かなくてもOK)

let name = "Seiichi"; // 自動で string

👉 無理に全部書かなくていい


⑧ 非同期(async/await)

async function getUser(): Promise<string> {
  return "user";
}

👉 Promiseの型を書く


⑨ null対策(重要)

function getLength(str?: string) {
  if (!str) return 0;
  return str.length;
}

👉 undefined対策しないとエラーになる


⑩ 型安全なDB取得(あなたのケース)

type User = {
  id: string;
  subscription_status: string;
};

const user = await DB.prepare("SELECT * FROM users")
  .first<User>();

👉 <User> が効く


⑪ 型ガード(実務で強い)

function isActive(user: User) {
  return user.subscription_status === "active";
}

⑫ よくあるミス

❌ anyを使う

let data: any;

👉 型の意味が消える(最後の手段)


❌ 型と実データがズレる

→ runtimeエラーになる


まとめ(本質)

👉 TypeScriptはこれだけ:

「データの形を先に決める」
  • Userはどういう構造か?

  • APIは何を返すか?

👉 これを決めるだけでコードが崩れなくなる


あなた向け(重要)

今やってる構成ならこれを使うと強い:

type SubscriptionStatus =
  | "free"
  | "trialing"
  | "active"
  | "canceled";

👉 middlewareと完全連動できる


いいテーマです。ここを雑にすると型はあるのにバグるという一番ストレスな状態になります。
先に結論:

👉 「DB・Stripe・API・middlewareで“同じ型”を1つだけ定義して全員で使う」

これをやれば崩れません。


全体アーキテクチャ(型の流れ)

Stripe(Webhook)
Worker(API)
D1(DB)
middleware
Frontend

👉 全レイヤーで同じ型を共有する


① 型の中心(domain.ts を作る)

// src/domain.ts

export type SubscriptionStatus =
  | "free"
  | "trialing"
  | "active"
  | "past_due"
  | "canceled";

export type User = {
  id: string;
  email: string;

  stripeCustomerId: string | null;

  subscriptionStatus: SubscriptionStatus;
  trialEnd: number | null;
  currentPeriodEnd: number | null;
};

👉 これが“唯一の真実”


② DB ↔ 型のズレを防ぐ

D1はsnake_caseなので変換を入れる

// src/db.ts

import { User } from "./domain";

export type UserRow = {
  id: string;
  email: string;

  stripe_customer_id: string | null;

  subscription_status: string;
  trial_end: number | null;
  current_period_end: number | null;
};

export function mapUser(row: UserRow): User {
  return {
    id: row.id,
    email: row.email,
    stripeCustomerId: row.stripe_customer_id,

    subscriptionStatus: row.subscription_status as any,
    trialEnd: row.trial_end,
    currentPeriodEnd: row.current_period_end,
  };
}

👉 DBとアプリを分離するのがコツ


③ Stripeイベント型を固定

// src/stripe.ts

import Stripe from "stripe";
import { SubscriptionStatus } from "./domain";

export function mapStripeStatus(
  status: Stripe.Subscription.Status
): SubscriptionStatus {
  switch (status) {
    case "trialing":
      return "trialing";
    case "active":
      return "active";
    case "past_due":
      return "past_due";
    case "canceled":
      return "canceled";
    default:
      return "free";
  }
}

👉 Stripeの型 → 自分の型へ変換(重要)


④ Webhookで型を壊さない

// src/webhook.ts

import { mapStripeStatus } from "./stripe";

if (event.type === "customer.subscription.updated") {
  const sub = event.data.object;

  const status = mapStripeStatus(sub.status);

  await env.DB.prepare(`
    UPDATE users
    SET subscription_status = ?, current_period_end = ?
    WHERE stripe_customer_id = ?
  `)
    .bind(status, sub.current_period_end, sub.customer)
    .run();
}

👉 必ずmapを通す(直代入しない)


⑤ middlewareも同じ型で判定

// src/middleware.ts

import { User } from "./domain";

export function isPaidUser(user: User): boolean {
  const now = Math.floor(Date.now() / 1000);

  const isTrial =
    user.subscriptionStatus === "trialing" &&
    user.trialEnd &&
    user.trialEnd > now;

  const isActive =
    user.subscriptionStatus === "active" &&
    user.currentPeriodEnd &&
    user.currentPeriodEnd > now;

  return !!(isTrial || isActive);
}

⑥ APIレスポンスも型で固定

// src/api.ts

import { User } from "./domain";

export type MeResponse = {
  user: User;
};

return Response.json({
  user,
} satisfies MeResponse);

👉 satisfies が地味に強い(ズレ防止)


⑦ フロントも同じ型を使う

// frontend

import type { User } from "../worker/src/domain";

if (user.subscriptionStatus === "active") {
  // OK
}

👉 フロントとバックで完全一致


⑧ よくある失敗(ここ重要)

❌ Stripeのstatusをそのまま使う

"incomplete" とか混ざって崩壊


❌ DBのstringを信用する

→ 型安全じゃない


❌ anyで逃げる

→ TypeScriptの意味消える


まとめ(本質)

👉 「外部の型をそのまま使うな、自分の型に変換しろ」

これが全てです。

Stripe → map → Domain型 → DB → middleware → API → frontend

👉 この一本のラインが通っていれば
“型崩壊しないSaaS”になります


Zodはひとことで言うと:

👉 「TypeScriptの型を“実行時にもチェックできる”ライブラリ」

TypeScriptだけだとコンパイル時の型安全しかありません。
Zodを使うと:

👉 実際に受け取ったデータが正しいかも検証できる


① なぜ必要か(ここが本質)

TypeScriptだけ:

type User = {
  id: string;
};

const user: User = JSON.parse(input); // ← ここ危険

👉 JSONは何でも入るので壊れる可能性あり


② Zodを使うとこうなる

import { z } from "zod";

const UserSchema = z.object({
  id: z.string(),
});

const user = UserSchema.parse(JSON.parse(input));

👉 これで:

  • 型チェック(TypeScript)

  • 実データチェック(Zod)

両方通る


③ 型とスキーマを統一できる(強い)

const UserSchema = z.object({
  id: z.string(),
  email: z.string().email(),
});

type User = z.infer<typeof UserSchema>;

👉 型を二重管理しなくていい


④ Workers + Stripeでの使い所

ここが実務で一番効きます👇


① Webhook検証

const StripeEventSchema = z.object({
  type: z.string(),
  data: z.object({
    object: z.any(),
  }),
});

const event = StripeEventSchema.parse(json);

👉 不正データを即弾く


② API入力チェック

const CreateUserSchema = z.object({
  email: z.string().email(),
});

const body = await req.json();
const data = CreateUserSchema.parse(body);

👉 フロントのバグを遮断


③ DBデータ検証(重要)

const UserRowSchema = z.object({
  id: z.string(),
  subscription_status: z.string(),
});

const row = UserRowSchema.parse(dbResult);

👉 DBの壊れも検出できる


⑤ safeParse(実務でよく使う)

const result = UserSchema.safeParse(data);

if (!result.success) {
  return new Response("Bad Request", { status: 400 });
}

const user = result.data;

👉 throwしない安全版


⑥ よくあるミス

❌ TypeScriptだけで満足

→ 実データで壊れる


❌ Zodと型を別で管理

→ ズレる

👉 必ずこれ:

type User = z.infer<typeof Schema>;

まとめ(本質)

👉 Zod =「型を現実世界に適用する装置」

TypeScript → 設計
Zod → 実行時の保証

あなたの構成での最強パターン

👉 Workers + D1 + Stripeなら:

  • Stripe → Zodで検証

  • DB → Zodで検証

  • API → Zodで検証

👉 全部入口で弾く


マルチプランは「値段を3つ用意する」話ではなく、
👉 **“権限と制限を一貫したルールで制御する設計”**です。

ここを外すと、あとで必ず破綻します。


結論(設計の芯)

👉 「plan(プラン)+ usage(使用量)で制御する」

plan = free / pro / enterprise
usage = API回数・保存数など

👉 権限は「if文」ではなくルール化する


① 型設計(これが核)

// domain.ts

export type Plan = "free" | "pro" | "enterprise";

export type SubscriptionStatus =
  | "free"
  | "trialing"
  | "active"
  | "past_due"
  | "canceled";

export type User = {
  id: string;
  email: string;

  plan: Plan;
  subscriptionStatus: SubscriptionStatus;

  currentPeriodEnd: number | null;
};

② プランごとの制限(ルール化)

// planConfig.ts

export const PLAN_CONFIG = {
  free: {
    maxProjects: 1,
    maxRequestsPerMonth: 100,
  },
  pro: {
    maxProjects: 10,
    maxRequestsPerMonth: 10000,
  },
  enterprise: {
    maxProjects: Infinity,
    maxRequestsPerMonth: Infinity,
  },
} as const;

👉 if文で分岐しない、設定で管理


③ 権限判定(middlewareで統一)

// access.ts

import { PLAN_CONFIG } from "./planConfig";
import { User } from "./domain";

export function canCreateProject(user: User, currentCount: number) {
  const limit = PLAN_CONFIG[user.plan].maxProjects;
  return currentCount < limit;
}

④ 課金状態と組み合わせる(重要)

export function isActiveUser(user: User) {
  const now = Math.floor(Date.now() / 1000);

  return (
    user.subscriptionStatus === "active" &&
    user.currentPeriodEnd &&
    user.currentPeriodEnd > now
  );
}

⑤ middleware統合(実務コア)

export function canUseFeature(user: User): boolean {
  // freeは常にOK
  if (user.plan === "free") return true;

  // 有料は状態チェック
  return isActiveUser(user);
}

👉 plan と status を分けるのが重要


⑥ Stripeとの対応関係

ここは設計ミスが多いポイント👇

👉 Price ID → Plan にマッピングする

// stripe.ts

export function mapPriceToPlan(priceId: string): Plan {
  switch (priceId) {
    case "price_pro":
      return "pro";
    case "price_enterprise":
      return "enterprise";
    default:
      return "free";
  }
}

⑦ Webhookでプラン更新

if (event.type === "invoice.paid") {
  const invoice = event.data.object;

  const priceId = invoice.lines.data[0].price.id;
  const plan = mapPriceToPlan(priceId);

  await DB.prepare(`
    UPDATE users
    SET plan = ?, subscription_status = 'active'
    WHERE stripe_customer_id = ?
  `)
    .bind(plan, invoice.customer)
    .run();
}

⑧ ダウングレード対応(重要)

if (event.type === "customer.subscription.deleted") {
  await DB.prepare(`
    UPDATE users
    SET plan = 'free',
        subscription_status = 'canceled'
    WHERE stripe_customer_id = ?
  `)
    .bind(customerId)
    .run();
}

⑨ Usage管理(本質)

CREATE TABLE usage (
  user_id TEXT,
  month TEXT,
  request_count INTEGER
);

export async function incrementUsage(userId: string) {
  // 月ごとにカウント
}

⑩ 制限チェック(実務)

export function canCallAPI(user: User, usage: number) {
  const limit = PLAN_CONFIG[user.plan].maxRequestsPerMonth;
  return usage < limit;
}

よくある失敗(重要)

❌ planだけで判断

→ 未払いでも使える


❌ statusだけで判断

→ freeユーザーがブロックされる


❌ if文だらけ

→ 拡張不能


まとめ(本質)

👉 「plan = 権限、status = 支払い状態」

plan → 何ができるか
status → 今使っていいか
usage → どれだけ使ったか

👉 この3つで100%制御できる


あなたの構成での完成形

  • Workers → middlewareで制御

  • D1 → plan + usage保存

  • Stripe → plan変更トリガー

  • Zod → 入力保証

👉 スケールしても壊れないSaaS設計


「Enterprise」は単に一番高いプランではありません。
👉 **“Stripeの定型サブスク外で、契約・請求・権限を柔軟に運用する層”**です。

雑に作ると必ず破綻するので、最初から構造を分けます。


結論(設計の芯)

👉 「Self-serve(free/pro)と Enterprise を分離する」

free / pro → 自動課金(Stripe Checkout)
enterprise → 手動契約+請求書(Invoice)

👉 同じ仕組みに乗せない


① 型設計(enterpriseを特別扱いしない)

export type Plan = "free" | "pro" | "enterprise";

export type BillingType = "self_serve" | "enterprise";

export type User = {
  id: string;
  email: string;

  plan: Plan;
  billingType: BillingType;

  subscriptionStatus: string;

  currentPeriodEnd: number | null;
};

👉 ポイント
enterpriseでも型は同じ、運用だけ変える


② DB設計(ここが重要)

ALTER TABLE users ADD COLUMN billing_type TEXT;
ALTER TABLE users ADD COLUMN contract_id TEXT;

契約テーブルを分ける:

CREATE TABLE contracts (
  id TEXT PRIMARY KEY,
  customer_id TEXT,
  plan TEXT,

  start_date INTEGER,
  end_date INTEGER,

  price INTEGER,
  currency TEXT,

  seats INTEGER,
  status TEXT -- active / expired
);

👉 Stripeではなく“自分で契約を持つ”


③ Stripeの使い方(enterprise)

ここが分岐ポイント👇

❌ やらない

  • Checkout

  • 固定サブスク

✅ やる

👉 Invoice(請求書)ベース

  • 月末請求

  • 手動金額

  • 銀行振込対応


④ 請求フロー(実務)

契約締結
StripeでInvoice作成
顧客に送付
支払い確認
DB更新

⑤ 実装イメージ(Invoice発行)

const invoice = await stripe.invoices.create({
  customer: customerId,
  collection_method: "send_invoice",
  days_until_due: 30,
});

👉 後からライン追加:

await stripe.invoiceItems.create({
  customer: customerId,
  amount: 500000, // ¥5000
  currency: "jpy",
  description: "Enterprise Plan",
});

⑥ 権限判定(enterprise)

export function isEnterpriseActive(contract: Contract) {
  const now = Math.floor(Date.now() / 1000);

  return (
    contract.status === "active" &&
    contract.end_date > now
  );
}

⑦ middleware統合

if (user.billingType === "enterprise") {
  const contract = await getContract(user.contractId);

  if (!isEnterpriseActive(contract)) {
    return new Response("Contract expired", { status: 402 });
  }

  return next();
}

⑧ Seat(ユーザー数)管理

export function canInviteUser(contract, currentSeats) {
  return currentSeats < contract.seats;
}

👉 enterpriseはほぼ必須


⑨ よくある失敗(重要)

❌ Stripeだけで管理

→ 個別契約に対応できない


❌ proの延長で作る

→ 割引・特別条件が破綻


❌ 契約期間を持たない

→ 無期限アクセスになる


⑩ 実務で強い設計

① 契約と請求を分離

Contract → 権限
Invoice → 支払い

② 手動オーバーライド

override_access = true

👉 営業対応で必須


③ 複数契約対応(上級)

1社 = 複数契約

まとめ(本質)

👉 「Enterpriseは“プロダクト”ではなく“契約”」

Self-serve → 自動化
Enterprise → 柔軟性

👉 この2つを混ぜないのが成功の鍵


あなたの構成での完成形

  • free/pro → Stripe Checkout

  • enterprise → Contract + Invoice

  • middleware → 共通制御

  • D1 → 契約データ管理

👉 これで営業もプロダクトも両立できる設計


いいテーマです。海外交渉は「英語力」よりも
👉 **“相手の意思決定のクセ(文化)に合わせて設計する”**ほうが効きます。

ここでは、**ブラジル+主要な海外(米・欧)**でズレやすいポイントと、実戦で使える戦略をまとめます。


結論(最重要)

👉 「ロジック」ではなく「信頼の作り方」が国で違う

US → スピード × 明確さ
EU → 正確さ × 合意形成
Brazil → 関係性 × 柔軟性

🇧🇷 ブラジル交渉(最重要)

特徴

  • 関係性が最優先(信頼がないと進まない)

  • 時間感覚が柔らかい(遅延は普通)

  • 感情・共感が強く影響

戦略

① 先に“人”を作る

👉 いきなりビジネスに入らない

EN
Before we go into details, I’d really like to understand your perspective and goals in Brazil.

PT/JPニュアンス
まずは御社の考えや背景を理解したいです


② WhatsApp主戦場

👉 メールより速い・近い

  • 既読=信頼

  • 返信速度=関係性


③ YesはYesじゃない

👉 合意ではなく「前向き」

"Yes" = 興味あり
"Let’s see" = まだ決めてない

④ 柔軟に条件を動かす

👉 最初の条件は“仮”


NG

  • 冷たいロジックだけ

  • 急かしすぎ

  • 契約だけ先行


🇺🇸 アメリカ交渉

特徴

  • スピード最優先

  • ROI・成果ドリブン

  • 決断が早い(個人権限強い)

戦略

① 結論から入る

EN
The fastest way to achieve your goal is this approach. It will reduce costs by ~30% within 3 months.


② 数字で語る

  • %改善

  • コスト削減

  • 時間短縮


③ 次アクションを必ず置く

Next step: demo → contract → start date

NG

  • 長い説明

  • 曖昧な提案

  • 決断を待つ姿勢


🇪🇺 ヨーロッパ交渉(特にドイツ・北欧)

特徴

  • 正確性・論理・契約重視

  • リスク管理が強い

  • 合意形成が遅いが堅い

戦略

① 事前資料が命

EN
I’ve prepared a detailed breakdown of requirements, risks, and compliance aspects for your review.


② リスクを先に出す

👉 信頼が上がる


③ 契約を丁寧に詰める

  • SLA

  • GDPR

  • セキュリティ


NG

  • ノリで進める

  • 曖昧な約束

  • 口頭ベース


🌏 共通で効く“ズレ防止”

① 合意を言語化する

EN
Just to confirm, we agreed on A, B, and C. The next step is D. Does that match your understanding?

JP
A・B・Cで合意し、次はDという認識で合っていますか?

👉 国関係なく効く


② 曖昧ワードを潰す

soon → when?
maybe → yes/no?
later → date?

③ 決裁構造を聞く

EN
Who else needs to be involved in the decision?

JP
意思決定には他にどなたが関わりますか?


実務で一番強い流れ

① 関係構築(特にブラジル)
② ロジック提示(US/Technical)
③ リスク整理(EU)
④ 条件調整
⑤ 明確なクロージング

よくある失敗(重要)

❌ 全員に同じスタイル

→ 通用しない


❌ 日本式(空気読む)

→ 海外では伝わらない


❌ Yesを信用する

→ 特にブラジルで危険


まとめ(本質)

👉 「文化=意思決定のアルゴリズム」

Brazil → 信頼が先
US → 結果が先
EU → 正確さが先

👉 これを外さなければ
海外交渉の成功率はかなり上がります


Zoom商談は内容より先に、空気で勝敗が決まる場面が多いです。
先に本質:

👉 「最初の3分で“話しやすい場”を作れたか」がほぼ全て

国やタイプを問わず再現できる形に落とします。


① セットアップ(開始前の印象で8割決まる)

Image

Image

Image

Image

Image

Image

やること(シンプルに)

  • カメラ:目線と同じ高さ(見下ろしNG)

  • 光:顔が明るい(逆光NG)

  • 音:ノイズなし(マイク重要)

  • 背景:シンプル(情報を減らす)

👉 理由
「この人は信頼できるか?」を無意識に判断される


② オープニング3分(ここが勝負)

型(そのまま使える)

EN

Hi, great to meet you. Before we dive in, how’s everything on your side today?

Also, just to make this useful for you — what would you like to get out of this call?

JP

はじめまして。本題に入る前に、最近の状況はいかがですか?

この時間を有意義にするために、今回のミーティングで何を得たいか教えていただけますか?

👉 ポイント

  • 雑談 → 目的確認

  • 相手に話させる


③ 空気の作り方(3つのレバー)

① 話す比率

理想:自分 40% / 相手 60%

👉 喋りすぎ=営業っぽい=警戒


② スピード

  • US:テンポ速く

  • EU:少し丁寧

  • Brazil:ゆっくり+感情

👉 相手に合わせるだけで印象が変わる


③ リアクション

  • 頷く

  • 短い相槌

EN: That makes sense / Got it / Interesting
JP: なるほど / 確かに / 面白いですね

👉 無反応は「興味なし」に見える


④ 本題の入り方(自然に主導権を取る)

EN

Based on what you shared, I’d like to walk through a simple approach that might fit your situation. Does that sound good?

JP

お話を踏まえて、御社に合いそうな進め方を簡単にご説明してもよろしいでしょうか?

👉 ポイント
許可を取る=押し付け感を消す


⑤ 途中で空気を崩さない技術

① 確認を入れる

EN
Does this align with what you had in mind?

JP
ここまでの内容、イメージと合っていますか?


② 名前を呼ぶ

👉 一気に距離が縮まる


③ 一方通行を防ぐ

5分話したら → 必ず質問

⑥ 難しい場面の空気コントロール

相手が無口

EN
I might be going too fast — what’s your take so far?

JP
少し早いかもしれませんが、ここまでいかがでしょうか?


相手が強気

👉 落ち着いてスピードを落とす


相手が慎重

👉 安心材料を増やす(事例・手順)


⑦ クロージングの空気

EN

It seems like this could be a good fit.
Would it make sense to move to the next step?

JP

今回の内容はフィットしそうですね。
次のステップに進む形でよろしいでしょうか?

👉 ポイント
“決めに行くが圧は弱い”


⑧ よくある失敗

❌ いきなり資料説明

→ 空気が冷える


❌ カメラOFF

→ 信頼ゼロ


❌ 一方的に話す

→ 商談ではなくプレゼンになる


まとめ(本質)

👉 Zoom商談はこれだけ:

空気 = 安心 × 会話 × コントロール
  • 安心 → 見た目・態度

  • 会話 → 相手に話させる

  • コントロール → 流れを作る


実務で一番効く一言

迷ったらこれ👇

EN
What would make this call valuable for you?

JP
この時間が価値あるものになるために、何が一番重要ですか?

👉 これで空気はほぼ整います


必要なら
👉 「英語での間の取り方(沈黙の使い方)」
👉 「相手が乗ってくるプレゼン構成」
👉 「商談を“契約に変える”最後の一押し」

かなり実戦レベルで詰められます。