sawa_tech’s blog

Web エンジニア。Java, Python, AWS などやっています。基本腰痛

Strands Agents の TypeScript SDK (プレビュー)でツール定義と実行を試してみた

はじめに

こんにちは、sawa です。
最近 AI 系のサービスにハマっており、Strands Agents に TypeScript SDK のプレビューが公開されたということで、いくつか検証を進めています。
今回は、その中でも「ツールの作成と実行」にフォーカスして触ってみたいと思います。
Strands Agents の TypeScript SDK(プレビュー)でツールを定義し、エージェントがそれを選択・実行する流れを、最小構成のコードで確認していきます。
Python SDK とは記述方法も異なるため、TypeScript SDK ではどのような実装になるのかを見ていきましょう。

セットアップ

前回のブログで Strands Agents の TypeScript SDK を動かせるように準備したのでこちらをご覧ください。

sawa-tech.hatenablog.com

TypeScript SDK のツール定義の例

Strands Agents の TypeScript SDK でツールを定義は以下のように行います。

import { Agent, tool } from '@strands-agents/sdk'
import z from 'zod'

// デモ用ツール
const demoTool = tool({
    name: 'demo', // LLM がツールを識別するための一意の名前
    description: 'デモ実行用のツール', // ツールの説明
    inputSchema: z.object({ // ツールに渡すプロパティを定義
        name: z.string(),
        id : z.number()
    }),
    callback: (input) => { // ツールの処理を記述
    return `demo tool. ${input.name}`
  },
})

const agent = new Agent({
    tools:[demoTool], // ツールをリストで渡す
    model: 'amazon.nova-lite-v1:0' // モデルの指定
});

await agent.invoke('デモを実行して') // AI エージェントがタスクを実行

これで AI エージェントにツールを渡し、ユーザーの指示に応じてエージェントが適切なツールを選択・実行できるようになります。
ここで使用している zod は、入力データの構造を定義し、実行時に検証を行うための TypeScript 向けスキーマ定義ライブラリです。
zod については以下のリファレンスをご覧ください。

zod.dev

予定管理 AI エージェントの作成(簡易実装)

色々触ってみるとやはりどこかで活用できそうなツールをもつ AI エージェントを実装してみたくなるものです。
ということで今回は予定を追加、管理してくれる AI エージェントを試しに作成してみました。

作成する AI エージェントの概要

今回作成する AI エージェントを図にしてみました。

実際のコード

今回は以下のように非常に簡易的に実装をしてみました。
toolAgent.ts

import { Agent, tool } from '@strands-agents/sdk'
import z from 'zod'

// スケジュールの型を定義
interface Schedule {
    date: string;
    title: string;
}

// スケジュールを持つリストを定義
const schedules: Schedule[] = [{
    date: '2026-01-10',
    title: 'ミーティング'
}]

// 指定した日付のスケジュールを確認するツール
const checkScheduleTool = tool({
    name: 'check_schedule',
    description: 'Check the schedule for the specified date',
    inputSchema: z.object({
        date: z.string().describe('The date of schedule'),
    }),
    callback: (input) => {
        // 指定した日付と一致する予定を抽出
        const checkDateResult = schedules.filter(s => 
                s.date == input.date
            )
        // リストの要素が 0 なら予定なし
        if(checkDateResult.length === 0){
            return `No schedule found for ${input.date}`
        }
    
    return `I already have a schedule for ${input.date}`
  },
})

// 指定した日付に予定を追加するツール
const addScheduleTool = tool({
    name: 'add_schedule',
    description: 'Add a schedule on the specified date',
    inputSchema: z.object({
        date: z.string().describe('The date of schedule'),
        title: z.string().describe('The title of schedule')
    }),
    callback: (input) => {
        const schedule = {
            date: input.date,
            title: input.title,
        }
        // 予定の追加
        schedules.push(schedule)
    
    return `I scheduled a ${input.title} for the ${input.date}`
  },
})

// エージェントの定義
const agent = new Agent({
    tools:[checkScheduleTool, addScheduleTool],
    model: 'amazon.nova-lite-v1:0'
});

// 実行
await agent.invoke("2026年1月7日に美容院の予定をいれて")
await agent.invoke("2026年1月10日の予定が空いているか教えて")


では、このコードを実行してみます。
実行コマンド

npx tsx toolAgent.ts 

実行結果(ツール選択と実行ログ)

<thinking>The User wants to schedule an appointment at a beauty salon for January 7, 2026. I need to use the 'add_schedule' tool to accomplish this. The required arguments are 'date' and 'title'.</thinking>

🔧 Tool #1: add_schedule
✓ Tool completed
<thinking>I successfully used the 'add_schedule' tool to schedule a beauty salon appointment for January 7, 2026. I should now inform the User of the successful scheduling.</thinking>

I have successfully scheduled a beauty salon appointment for January 7, 2026. Is there anything else you would like to add to your schedule?<thinking>The User wants to check if there is any schedule for January 7, 2026. I need to use the 'check_schedule' tool to accomplish this. The required argument is 'date'.</thinking> 
🔧 Tool #2: check_schedule
✓ Tool completed
<thinking>I successfully used the 'check_schedule' tool and found out that there is already a schedule for January 7, 2026. I should now inform the User of the schedule.</thinking>

うーん、何言ってるかわからん!
ということで翻訳してみました。

<thinking>ユーザーは2026年1月7日に美容院の予約をスケジュールしたいと考えています。これを実現するには「add_schedule」ツールを使用する必要があります。必要な引数は「date」と「title」です。</thinking>

🔧 ツール #1: add_schedule
✓ ツール完了
<thinking>「add_schedule」ツールを使用して、2026年1月7日の美容院予約を正常にスケジュールしました。ユーザーに予約成功を通知する必要があります。</thinking>

2026年1月7日の美容院予約を正常にスケジュールしました。他にスケジュールに追加したいことはありますか?<thinking>ユーザーは2026年1月7日に予約があるか確認したいようです。これを実現するには「check_schedule」ツールを使用する必要があります。必要な引数は「date」です。</thinking> 
🔧 ツール #2: check_schedule
✓ ツール完了
<thinking>「check_schedule」ツールを正常に使用し、2026年1月7日に既に予約が入っていることを確認しました。ユーザーにこの予約内容を通知する必要があります。</thinking>

おぉ、ちゃんとツールを使ってタスクを実行してくれています!

さいごに

いかがだったでしょうか?
Strands Agents の TypeScript SDK はまだプレビュー版のため、基本的な機能は使用できますが、一部の機能は未サポートとなっています。(2026年1月現在)
PythonSDK と比較して記述方法も異なりますが、TypeScript のサポート開始によって間口は大きく広がったと思います。
今回はツール実行にフォーカスしましたが、Strands Agents の TypeScript SDK でこれからも遊んでいきたいと思います。

【備忘録】Strands Agents TypeScript SDK(プレビュー)を動かしてみたらQuick Startで詰まった話

はじめに

こんにちは、sawa です。
最近 AI 系のサービスにハマっており、Strands Agents に TypeScript SDK のプレビューが公開されたということで、実際に動かしてみることにしました。
しかし、ドキュメントのクイックスタートの手順通りに実行してもうまく動かなかったので備忘録的に解消の流れを残しておきます。

セットアップ

前提

今回の手順は Strands Agents のドキュメントにある TypeScript SDK のクイックスタートを参考に進めていきます。

strandsagents.com

また、ドキュメントにも記載がある通り以下の環境の準備が必要です。

まず、Node.js 20以上とnpmがインストールされていることを確認してください。インストール手順については、npmのドキュメントをご覧ください。

npm のドキュメントはこちら

また、私の環境は以下の通りです。

  • node : v25.2.1
  • npm : 11.6.2
  • aws cli : インストール済み

プロジェクトのセットアップ

環境の設定ができたらプロジェクトを作成していきます。
まずはプロジェクトのルートとなるフォルダを作成します。

mkdir my-agent

フォルダが作成できたら作成したフォルダに移動します。

cd my-agent

これでプロジェクトのルートに移動できました。 次にプロジェクトの初期化を行います。

npm init -y

これでプロジェクトの初期化が完了しました。
つづいて、@strands-agents/sdk パッケージをインストールします。

npm install @strands-agents/sdk

パッケージがインストールできたらソースファイルを作成していきます。

// src ディレクトリの作成
mkdir src
// ts ファイルの作成
touch src/agent.ts

これでプロジェクトのセットアップは完成しました。
現時点のディレクトリ構造は以下の通りです。

my-agent/
├── node_modules/
│   └── 量が多いので省略
├── src/
│   └── agent.ts
├── package.json
└── package-lock.json


ドキュメントには aws 関連のセットアップとして資格情報の設定がありますが、私はすでに aws cli を使用していたためセットアップが不要でした。
未セットアップの方はドキュメントを参照して設定してみてください。

エージェントの実行

これで準備が終わったので実際にコードを動かしてみます。 agent.ts で公式ドキュメントのサンプルコードをもとに、実際に動作確認を行っていきます。

// Define a custom tool as a TypeScript function
import { Agent, tool } from '@strands-agents/sdk'
import z from 'zod'

const letterCounter = tool({
  name: 'letter_counter',
  description: 'Count occurrences of a specific letter in a word. Performs case-insensitive matching.',
  // Zod schema for letter counter input validation
  inputSchema: z
    .object({
      word: z.string().describe('The input word to search in'),
      letter: z.string().describe('The specific letter to count'),
    })
    .refine((data) => data.letter.length === 1, {
      message: "The 'letter' parameter must be a single character",
    }),
  callback: (input) => {
    const { word, letter } = input

    // Convert both to lowercase for case-insensitive comparison
    const lowerWord = word.toLowerCase()
    const lowerLetter = letter.toLowerCase()

    // Count occurrences
    let count = 0
    for (const char of lowerWord) {
      if (char === lowerLetter) {
        count++
      }
    }

    // Return result as string (following the pattern of other tools in this project)
    return `The letter '${letter}' appears ${count} time(s) in '${word}'`
  },
})

// Create an agent with tools with our custom letterCounter tool
const agent = new Agent({
  tools: [letterCounter],
})

// Ask the agent a question that uses the available tools
const message = `Tell me how many letter R's are in the word "strawberry" 🍓`
const result = await agent.invoke(message)
console.log(result.lastMessage)

エラー1. Top-level await の問題

これで準備ができたので早速動かしてみます。

npx tsx src/agent.ts

しかし、上記のコマンドを実行すると以下のエラーが出てしまいました。

node:internal/modules/run_main:107
    triggerUncaughtException(
    ^
Error: Transform failed with 1 error:
/Users/r-osawa/Desktop/開発/TypeScript/study/my-agent/src/agent.ts:44:15: ERROR: Top-level await is currently not supported with the "cjs" output format
    at failureErrorWithLog (/Users/r-osawa/.npm/_npx/fd45a72a545557e9/node_modules/esbuild/lib/main.js:1467:15)
    at /Users/r-osawa/.npm/_npx/fd45a72a545557e9/node_modules/esbuild/lib/main.js:736:50
    at responseCallbacks.<computed> (/Users/r-osawa/.npm/_npx/fd45a72a545557e9/node_modules/esbuild/lib/main.js:603:9)
    at handleIncomingPacket (/Users/r-osawa/.npm/_npx/fd45a72a545557e9/node_modules/esbuild/lib/main.js:658:12)
    at Socket.readFromStdout (/Users/r-osawa/.npm/_npx/fd45a72a545557e9/node_modules/esbuild/lib/main.js:581:7)
    at Socket.emit (node:events:508:28)
    at addChunk (node:internal/streams/readable:559:12)
    at readableAddChunkPushByteMode (node:internal/streams/readable:510:3)
    at Readable.push (node:internal/streams/readable:390:5)
    at Pipe.onStreamRead (node:internal/stream_base_commons:189:23) {
  name: 'TransformError'
}

うーん、なんだこのエラー、、
よく読んでみるとこんな文がありました。

ERROR: Top-level await is currently not supported with the "cjs" output format

ちなみに翻訳してみるとこんな感じ。

エラー: トップレベルの await は現在「cjs」出力形式ではサポートされていません

どうやら top-level await の使用が原因でエラーが発生していそうです。
色々調べてみて package.json の type の値を commonjs から module に変更したところこのエラーは解消しました。
修正後の package.json

{
  "name": "my-agent",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "module",
  "dependencies": {
    "@strands-agents/sdk": "^0.1.4"
  }
}

参考にしたページ

answers.netlify.com

エラー2. Anthropic モデルの問題

気を取り直して再度コマンドを実行してみます。
すると、今度はこんなエラーがでました。

ResourceNotFoundException: Model use case details have not been submitted for this account. Fill out the Anthropic use case details form before using the model. If you have already filled out the form, try again in 15 minutes.

翻訳するとこんな感じ

ResourceNotFoundException: このアカウントに対してモデル使用ケースの詳細が提出されていません。モデルを使用する前に、Anthropic使用ケース詳細フォームに記入してください。既にフォームを記入済みの場合は、15分後に再度お試しください。

これは Strands Agents のドキュメントにもあるように、agent のモデル指定を行わない場合、デフォルトで Anthropic の Claude 4 が使用される(2026年1月時点)ために発生するエラーです。
Amazon Bedrock のモデルアクセス有効化手順は廃止されましたが、Anthropic モデルの場合は別途手順を踏む必要があるようです。

Anthropicモデルの場合、初めてモデルにアクセスするユーザーは、ユースケースの詳細を送信する必要がある場合があります。

ということで、本当のクイックスタートになるように面倒な手順は飛ばしていきます。
要は Anthropic モデルを使わなければ発生しないエラーですので、model に Amazon Nova Lite を選択して実行してみます。
修正後の agent.ts

// Define a custom tool as a TypeScript function
import { Agent, tool } from '@strands-agents/sdk'
import z from 'zod'

const letterCounter = tool({
  name: 'letter_counter',
  description: 'Count occurrences of a specific letter in a word. Performs case-insensitive matching.',
  // Zod schema for letter counter input validation
  inputSchema: z
    .object({
      word: z.string().describe('The input word to search in'),
      letter: z.string().describe('The specific letter to count'),
    })
    .refine((data) => data.letter.length === 1, {
      message: "The 'letter' parameter must be a single character",
    }),
  callback: (input) => {
    const { word, letter } = input

    // Convert both to lowercase for case-insensitive comparison
    const lowerWord = word.toLowerCase()
    const lowerLetter = letter.toLowerCase()

    // Count occurrences
    let count = 0
    for (const char of lowerWord) {
      if (char === lowerLetter) {
        count++
      }
    }

    // Return result as string (following the pattern of other tools in this project)
    return `The letter '${letter}' appears ${count} time(s) in '${word}'`
  },
})

// Create an agent with tools with our custom letterCounter tool
const agent = new Agent({
  tools: [letterCounter],
  model: 'amazon.nova-lite-v1:0' // モデルを指定するように修正
})

// Ask the agent a question that uses the available tools
const message = `Tell me how many letter R's are in the word "strawberry" 🍓`
const result = await agent.invoke(message)
console.log(result.lastMessage)

これで再度実行してみます。
実行コマンド

npx tsx src/agent.ts

実行結果

<thinking> To determine the number of letter 'R's in the word "strawberry", I can use the tool 'letter_counter' which counts occurrences of a specific letter in a word. I will input the word "strawberry" and the letter 'R' into the tool.</thinking>

🔧 Tool #1: letter_counter
✓ Tool completed
<thinking> The tool 'letter_counter' has returned the result that the letter 'R' appears 3 times in the word "strawberry". I can now provide this information to the User.</thinking>

The letter 'R' appears 3 times in the word "strawberry" Message {
  type: 'message',
  role: 'assistant',
  content: [
    TextBlock {
      type: 'textBlock',
      text: `<thinking> The tool 'letter_counter' has returned the result that the letter 'R' appears 3 times in the word "strawberry". I can now provide this information to the User.</thinking>\n` +
        '\n' +
        `The letter 'R' appears 3 times in the word "strawberry" `
    }
  ]
}

サンプルでは「"strawberry" に R がいくつ含まれるか数えて」と LLM に依頼しており、回答として「R は 3 回出現しています」と返ってきているので無事実行ができました。

さいごに

今回は Strands Agents の TypeScript SDK のクイックスタートがうまく動かなかったので修正して動かしてみました。
ただ、TypeScript の SDK は2025年12月に発表されてまだプレビュー版となっています。 Python SDK で使用できる機能がまだサポートされていない場合があるため注意が必要です。(2026年1月時点)
個人的には型付けができる言語が好きなので TypeScript SDK のサポートが充実して今後の AI 開発が拡大することを願っています。

Java エンジニアが TypeScript を触ってみた(null / undefined 編)

はじめに

こんにちは、sawa です。
普段は Java を使用して開発を行っていますが、最近 TypeScript を学び始めました。
前回のブログでは ジェネリクス(総称型)について Java と TypeScript の違いについて整理したのでぜひ読んでみてください。

sawa-tech.hatenablog.com

今回は TypeScript の null 許容について Java と比較していきたいと思います。

Java の null の扱い

今回はクラス定義を例にして null の扱いを定義します。
簡易的に Person クラスを作成してみました。

class Person {

    String name;
    int age;

    void setName(String name) {
        this.name = name;
    }

    void setAge(int age) {
        this.age = age;
    }

    String getName() {
        return this.name;
    }

    int getAge() {
        return this.age;
    }

}

public class Main {
    public static void main(String[] args) {
        Person alice =  new Person();

        System.out.println(alice.getName()); // null
        System.out.println(alice.getAge()); // 0

        alice.setName("Alice");
        alice.setAge(25);

        System.out.println(alice.getName()); // Alice
        System.out.println(alice.getAge()); // 25
    }
}

Java の場合はインスタンスの初期化のタイミングでプロパティの値を指定しない場合はデフォルト値が各プロパティに格納されます。
たとえば、このコードの例では String 型には null, int 型には 0 が格納されています。
これは Java では、参照型はデフォルトで null が代入される仕様になっていると言えます。
では、TypeScript ではどうでしょうか?

TypeScript の null の扱い

TypeScript でも同じように Person クラスを作成していきます。

class Person {

    name: string;
    age: number;

    setName(name: string): void{
        this.name = name;
    }

    setAge(age: number): void{
        this.age = age;
    }

    getName(): string{
        return this.name;
    }

    getAge(): number{
        return this.age;
    }
}

let alice: Person = new Person();

console.log(alice.getName()); // undefined
console.log(alice.getAge()); // undefined

alice.setName("Alice");
alice.setAge(25);

console.log(alice.getName()); // Alice
console.log(alice.getAge()); // 25

※ ここでは strict モードを OFF にした状態での挙動を確認します。strict については後述で説明
実行結果を見てみると、インスタンス初期化時にプロパティに値を格納していない場合はデフォルト値は格納されず、undefined となります。

Java と TypeScript の違い

undefined については Java にはない概念で少し戸惑うかと思いますので null と undefined について整理してみます。

Java TypeScript
null 値がない状態を表す 明示的に値がない状態を表す
undefined なし 値が空(定義されていない)の状態を表す

Java の場合は null が値が空 or 未定義の状態を表します。
一方、TypeScript では、値が存在しない状態を表すために null と undefined が明確に区別されています。
null は「意図的に値が存在しない」ことを表し、undefined は「値がまだ定義されていない」状態を表します。

TypeScript で null / undefined を制限する

さきほどのコードではインスタンスの初期化時にプロパティの値を指定しなかったため、undefined となっていました。
しかし、null や undefined を安易に許容しているとバグの原因になりかねませんので これらを制限する仕組みが必要です。
Java では null チェックを行ったり、アノテーションを使用して null を制限したりすることが可能ですが、TypeScript ではどうでしょうか?
実は TypeScript では tsconfig.json というファイルでオプションを設定することで null / undefined を一律で制限することが可能です。
tsconfig.json をソースファイルと同じ階層に作成していきます。
tsconfig.json

{
  "compilerOptions": {
    "strict": true
  }
}

この状態でコンパイルを行うと以下のエラーが出ます。

test.ts:3:5 - error TS2564: Property 'name' has no initializer and is not definitely assigned in the constructor.

3     name: string;
      ~~~~

test.ts:4:5 - error TS2564: Property 'age' has no initializer and is not definitely assigned in the constructor.

4     age: number;
      ~~~


Found 2 errors in the same file, starting at: test.ts:3

このように tsconfig.json で設定することで一律で null / undefined を制限できます。
もう一つ例として null を入れようとしてもエラーになります。

class Person {

    name: string = "Alice";
    age: number = 25;

    setName(name: string): void{
        this.name = name;
    }

    setAge(age: number): void{
        this.age = age;
    }

    getName(): string{
        return this.name;
    }

    getAge(): number{
        return this.age;
    }
}

let alice: Person = new Person();

alice.setName(null); // コンパイルエラー
alice.setAge(25);

TypeScript の null 許容

tsconfig.json の設定で null / undefined を制限する方法を説明しましたが、一部のプロパティのみ null を許容したい場合はどうすれば良いでしょうか?
このような場合にはプロパティ名に ? を付与することで解決できます。
また、? を付与したプロパティは、型として「string | undefined」として扱われますので、getter の戻り値に undefined も追加します。

class Person {

    name?: string;
    age?: number;

    setName(name: string): void{
        this.name = name;
    }

    setAge(age: number): void{
        this.age = age;
    }

    // 戻り値に undefined も追加
    getName(): string | undefined{
        return this.name;
    }

    // 戻り値に undefined も追加
    getAge(): number | undefined{
        return this.age;
    }
}

let alice: Person = new Person();

console.log(alice.getName()); // undefined
console.log(alice.getAge()); // undefined

alice.setName("Alice");
alice.setAge(25);

console.log(alice.getName()); // Alice
console.log(alice.getAge()); // 25

さいごに

TypeScript の null 許容は、Java と比べると「より厳密に状態を区別する」設計になっていると感じました。
Java では参照型がデフォルトで null を持つのに対し、TypeScript では null と undefined を明確に分けて扱い、さらに strict オプションによってそれらを型レベルで制御できる点が大きな違いです。
TypeScript の null 設計を正しく理解することで、安全で読みやすいコードが書けるようになると思います。

Java エンジニアが TypeScript を触ってみた(ジェネリクス編 | 総称型・型制約・Javaとの違い)

はじめに

こんにちは、sawa です。
普段は Java を使って開発業務などを行っていますが、最近 AI 系の開発に興味を持ち TypeScript を学び始めました。
前回のブログでは Java と TypeScript のインタフェースの違いについて整理したのでぜひ読んでください。

sawa-tech.hatenablog.com

今回は、TypeScript のジェネリクス(総称型)について、Java と比較しながら整理していきます。
インタフェースやクラスは Java とよく似ていましたが、ジェネリクスも同じ感覚で扱えるのでしょうか?

Javaジェネリクス(総称型)とは?

ジェネリクスとはクラスで使用する型を安全に再利用できる仕組みです。
例えば、List インタフェースでは格納するデータ型を <> の中に指定できます。

List<String> list = new ArrayList<>();

このように、クラスやインタフェースの定義時には型を固定せず、利用時に型を指定できる仕組みがジェネリクスです。
では、まずは Java で簡単にジェネリクスを使用したコードを記述してみます。

package org.example;

class Data<T>{

    T content;

    public Data(T data){
        this.content = data;
    }

    public T getData(){
        return content;
    }
}

public class Main {
    public static void main(String[] args) {
        Data<String> strData = new Data<String>("test data");
        Data<Integer> intData = new Data<Integer>(12345);
        System.out.println(strData.getData()); // test data
        System.out.println(intData.getData()); // 12345
    }
}

Java ではこのように、クラス名の後ろに を付けてクラスを宣言し、T を汎用的な型として使用することが可能です。
このように実装をすることで Main クラスでは Data で使用する型を String や Integer などさまざまな型に変更できます。
では、TypeScript ではどうでしょうか?

TypeScript のジェネリクス

TypeScript も型付けが可能ですので、ジェネリクスを使用できます。
では、先ほど Java で記述したコードを TypeScript に変換してみます。

class Data<T> {

    content: T;

    constructor(content: T){
        this.content = content;
    }

    getContent(): T{
        return this.content;
    }
}

let strData: Data<string> = new Data<string>("test data");
let intData: Data<number> = new Data<number>(12345);
console.log(strData.getContent()); // test data
console.log(intData.getContent()); // 12345

これまたほぼ Java と一緒!
TypeScript も Java と同様にクラス名ジェネリクスを使用したクラスが作成可能です。
しかし、実は Java と TypeScript では右辺のジェネリクスの型指定の省略方法が異なります。
Java

// <> は必要だが型指定は省略可能
Data<String> strData = new Data<>("test data");
Data<Integer> intData = new Data<>(12345);

TypeScript

// 右辺は <> ごと省略可能
let strData: Data<string> = new Data("test data");
let intData: Data<number> = new Data(12345);

右辺の <> は残して型指定を省略しようとするとエラーが発生します。

Type argument list cannot be empty.

このように Java と TypeScript でジェネリクスに関する微妙な違いはありますが、この点を踏まえても、Java エンジニアであれば TypeScript のジェネリクスは比較的とっつきやすいと感じるのではないでしょうか。

型の制限

Java ではジェネリクスの T に入れられる型を制限できる仕組みがあります。
例えば、T に入れることができる型を X 型のサブクラスのみに限定したい場合は とすることで制限が可能です。
Java で T の型を Number 型のサブクラスに限定するように修正してみます。

package org.example;

class Data<T extends Number> {

    T content;

    public Data(T data){
        this.content = data;
    }

    public T getData(){
        return content;
    }
}

public class Main {
    public static void main(String[] args) {
        Data<String> strData = new Data<>("test data"); // NG
        Data<Integer> intData = new Data<>(12345); // OK
    }
}

実はこのジェネリクスの型の制限は TypeScript でも可能です!さらに、記述方法も Java と同じように書くことが可能なのです。
TypeScript で Java と似た実装にするために Number 型を継承した Integer クラスを作成してみます。

class Integer extends Number {

    getNum(): string{
        return this.toString();
    }
}

class Data<T extends Number> {
    content: T;

    constructor(content: T){
        this.content = content;
    }

    getContent(): T{
        return this.content;
    }
}

let strData: Data<string> = new Data("test data"); // NG
let num: Integer = new Integer(12345)
let intData: Data<Integer> = new Data(num); // OK
console.log(intData.getContent().getNum()) // 12345

少し周りくどい実装になってしまいましたが、Java と似たようにジェネリクスの型を制限できると伝わるかなと思います。
※ 実務ではこのように Number を継承したクラスを作ることは少ないですが、Java の extends 制約と同じ概念を示すための例として記載しています。また、Number はプリミティブの number とは別物です。

ただし、ここで注意していただきたいのは extends の場合は Java と同じように制限ができますが、super やワイルドカードを使用した型の制限は TypeScript ではできない点です。
ここでは扱いませんが、TypeScript では境界ワイルドカード(? extends / ? super)は存在せず、型引数の制約や代入可能性で同様の表現を行います。

さいごに

TypeScript のジェネリクスは、構文や使い方の面では Java と非常によく似ており、Java エンジニアであれば直感的に理解しやすい機能だと感じました。
一方で、型引数の省略ルールなどは Java とは微妙に異なるため、最初は戸惑うポイントでもあると思います。
ただ、これらの違いを理解してしまえば、TypeScript のジェネリクスJava と同じ感覚で安全に型を扱える強力な仕組みになります。

Java エンジニアが TypeScript を触ってみた(インタフェース編|Javaとの違いと構造的型付け)

はじめに

こんにちは、sawa です。
最近 TypeScript の学習を進めていて、Java との共通点をいくつか発見してきました。
前回はクラスの継承についてブログを書いたのでぜひ読んでみてください。

sawa-tech.hatenablog.com

今回はインタフェースについて Java との共通点および違いについて整理していきます。

TypeScript でインタフェースはどうやって定義するの?

まずは Java でこんなインタフェースを実装してみました。

interface Vehicle {

    void drive();
}

class Car implements Vehicle {

    String name;

    Car(String name) {
        this.name = name;
    }

    @Override
    public void drive(){
        System.out.println("driving now...");
    }
}

public class Main {
    public static void main(String[] args) {
        Car car = new Car("taxi");
        car.drive();
    }
}

非常にシンプルなインタフェースです。

では、これを TypeScript で書いてみるとどのような記述になるでしょうか?
以下は TypeScript で実装したインタフェースになります。

interface  Vehicle {

    drive(): void;
}

class Car implements Vehicle {

    name: string;

    constructor(name: string){
        this.name = name;
    }
    
    drive(){
        console.log("driving car now...")
    }
}

let car: Vehicle = new Car("taxi");

car.drive();

Java と一緒やんけ!
クラスの時もそうでしたが、TypeScript のインタフェースも Java とほぼ同じ書き方で定義できます。 また、implements でインタフェースの実装を行うのも一緒ですね。
インタフェースという概念も Java と非常に似ているのでインタフェースの継承もこんな感じでできてしまいます。

interface  Vehicle {

    drive(): void;
}

interface Car extends Vehicle {

    getColor(): string;
}

class Taxi implements Car {

    color:string;

    constructor(color: string){
        this.color = color;
    }

    getColor(): string{
        return this.color;
    }

    drive(): void{
        console.log("driving car now...")
    }

}

let car: Car = new Taxi("taxi");

car.drive();

この辺りで気をつける点で言えば override キーワードでしょう。
クラス継承の際はサブクラスでオーバーライドするメソッドの名前の前に override キーワードを付与することで Java の @Override のような記述をしていました。
Java はインターフェースを実装するクラスでも @Override を付与できますが、TypeScript ではインタフェース実装時には override キーワードは使用できないようです。
また、TypeScript のインタフェース実装の特徴として構造的型付けオブジェクトリテラルというものがあります。
この部分は Java と異なる部分ですので整理をしていきます。

Java と TypeScript のインタフェースの違い

TypeScript の構造的型付け(Structural Typing)とは?

構造的型付けとはざっくりいうと型の構造をチェックして互換性があるのか確認する仕組みです。
なにそれ? と思うかもしれませんが、この仕組みによってインタフェースを実装するクラスが implements で明示的にインタフェースを実装しなくてもインタフェースで定義したメソッドをすべて実装できていればインタフェース型にクラスのインスタンスを格納することが可能になります。
言葉で表現してもなかなか伝わらないのでコードを見ていきましょう。

interface  Vehicle {

    drive(): void;
}

// implements は記述しない
class Car {

    name: string;

    constructor(name: string){
        this.name = name;
    }

    getName(): string{
        return this.name;
    }
    
    // Vehicle インタフェースのメソッドをすべて実装できている
    drive(){
        console.log("driving car now...")
    }
}

// Vehicle 型に Car クラスのインスタンスを格納
let car: Vehicle = new Car("taxi");

car.drive();

implements で Vehicle インタフェースを指定していないにも関わらず、Car クラスを Vehicle インタフェース型に格納できているため、ここは Java エンジニアが一瞬戸惑う箇所かもしれません。
さらに、これはクラスの継承時も同じであり、extends を記述しなくても型の構造が一致していればそのクラスの型として扱うことが可能です。
ただし、これはクラスの継承関係ではなく、あくまで「型として代入可能かどうか」を判定している点に注意が必要です。

個人的に1番戸惑ったオブジェクトリテラル

最後に私が1番戸惑ったオブジェクトリテラルについて説明します。
TypeScript で書かれた参考書、技術書ではインタフェースをこのような形で使用しているのを見かけないでしょうか?

interface  Student {
    id:number;
    name: string;
}

// ここ
let student: Student = {
    id: 1,
    name: "Alice"
}

console.log(student.name);

最初にこの記述を見たときは TypeScript とJava でインタフェースの意味合いが違うのかな? と思ったほど見慣れない書き方でした。
しかし、これも構造的型付けという仕組みを理解すると納得感がありました。
左辺の型として Student インタフェースを指定し、右辺で Student インタフェースと互換性のあるオブジェクトを定義しています。
この際にインタフェースとオブジェクトの互換性をチェックしてオブジェクトをインタフェース型に格納しています。
Java エンジニア的な視点で言うとじゃあクラスで定義すればいいのではないか?と感じましたが、ロジックを持たないオブジェクト、特に JSON 型を扱う際にはデータの構造だけ共通化できればいいのでインタフェースの方が適切だと感じました。

さいごに

TypeScript のインタフェースは、構文や使い方こそ Java とよく似ていますが、構造的型付けという仕組みによって、考え方は少し異なる部分があると感じました。
特に、implements を書かなくてもインタフェース型として扱えたり、オブジェクトリテラルをそのままインタフェース型として利用できる点は、Java エンジニアにとって最初は戸惑いやすいポイントだと思います。
一方で、「データの構造だけを保証したい」という用途では非常に相性がよく、JSON を扱う場面やフロントエンド開発では合理的な設計だとも感じました。
TypeScript は Java に近い書き味を持ちながら、型の考え方や柔軟性は別物として理解する必要がある言語だと思います。今後も Java との違いに注目しながら、TypeScript の特徴を整理していきたいと思います。

Java エンジニアが TypeScript を触ってみた(クラス継承編)

はじめに

こんにちは、sawa です。
普段は Java エンジニアをしていますが、最近 TypeScript を触るようになりました。
前回はクラス定義についてブログを書いています。

sawa-tech.hatenablog.com

今回は TypeScript のクラス継承について、Java と比較しながら構文や考え方の違いを整理していきます。
クラス定義の場合はほとんど Java と同じでとっつきやすかったですが、継承に関しても同じなのでしょうか?

TypeScript のクラス継承ってどう書く?

まずは Java でクラス継承をしていきます。

class Vehicle {

    String name;

    Vehicle(String name) {
        this.name = name;
    }

    String getName() {
        return this.name;
    }
}

class Car extends Vehicle {

    String color;

    Car(String name, String color) {
        super(name);
        this.color = color;
    }

    void drive(){
        System.out.println("driving now...");
    }

}

public class Main {
    public static void main(String[] args) {
        Car car = new Car("taxi", "black");
        car.drive();
    }
}

では、同じ実装を TypeScript で書いていきます。

class Vehicle {

    name:string;

    constructor(name:string){
        this.name = name;
    }

    getName():string{
        return this.name;
    }
}

class Car extends Vehicle {

    color:string;

    constructor(name:string, color:string){
        super(name);
        this.color = color;
    }
    
    drive(){
        console.log("driving now...");
    }
}

let car:Car = new Car("taxi",  "black");

car.drive();

ほぼ一緒!
TypeScript のクラス継承は、構文・考え方ともに Java と非常によく似ています。
extends キーワードで親クラスを指定できるのがより Java らしさを感じます。
また、Java 同様に super で親クラスのものを呼び出すことができるので Java の感覚でサブクラスの定義が可能そうです。
ただし、Java は @Override でオーバーライドするメソッドを明示できますよね?これは TypeScript でも可能です。
drive メソッドを Vehicle にも定義してオーバーライドしたメソッドとして定義します。
TypeScript のオーバーライドは override キーワードをメソッド名の前に記述することで明示的にすることが可能です。
※ override キーワードは必須ではありませんが、意図しないメソッド定義を防ぐため付けておくと安全かと思います。

class Vehicle {

    name:string;

    constructor(name:string){
        this.name = name;
    }

    getName():string{
        return this.name;
    }

    drive(){
        console.log("driving now...")
    }
}

class Car extends Vehicle {

    color:string;

    constructor(name:string, color:string){
        super(name);
        this.color = color;
    }
    
    override drive(){
        console.log("driving car now...");
    }
}

let car:Car = new Car("taxi",  "black");

car.drive();

drive メソッドをオーバーライドしてみました。
override を付けることで、親クラスのメソッドを正しくオーバーライドしているかをコンパイル時に検証できます。
例えば、override キーワードで親クラスに定義していないメソッド名を指定するとコンパイルエラー(型チェックエラー)になります。
もう一度 Vehicle クラスの drive メソッドを削除してみます。

This member cannot have an 'override' modifier because it is not declared in the base class 'Vehicle'.

このように Java と同じようにオーバーライドができるのも非常にわかりやすくて助かります。

補足

Java は多重継承が禁止(単一継承)ですが、これは TypeScript も同じです。
この辺りの考え方が同じなのも Java エンジニアにとってはありがたいですね!

さいごに

TypeScript のクラス継承やオーバーライドは、Java エンジニアにとって非常に馴染みやすく、学習コストが低いと感じました。
次回は interface 周りについても、Java との違いを整理してみたいと思います。

Java エンジニアが TypeScript を触ってみた(クラス定義編)

はじめに

こんにちは、sawa です。
最近 AI 系の実装にハマっており、これまでは Java で AI エージェントの実装などを行っていました。
ただし、AI 系のフレームワークPython や TypeScript の方が充実しているため、TypeScript の学習を進めています。
Java → TypeScript に触れてみて意外ととっつきやすい部分があったり Java との違いに戸惑った部分もあるのでブログにしていきたいと思います。
今回は Java エンジニアが TypeScript のクラス定義を学んだ時の感想について書いていきます。

TypeScript のクラス定義

まずは Java で Student というクラスを定義してみます。

class Student {
    int id;
    String name;

    Student(int id, String name) {
        this.id = id;
        this.name = name;
    }

    String getName() {
        return this.name;
    }
}

public class Main {
    public static void main(String[] args) {
        Student student = new Student(1, "Alice");
        System.out.println(student.getName());
    }
}

Student クラスではプロパティ、コンストラクタ、name を取得する get メソッドを定義しました。
このクラスを TypeScript で定義すると以下のようになります。

class Student {

    id:number;
    name:string;

    constructor(id:number,name:string){
        this.id = id;
        this.name = name;
    }

    getName():string{
        return this.name;
    }
}

let student = new Student(1,  "Alice");
console.log(student.getName());

はい、これが TypeScript のクラス定義です。
これを見て私はこのように感じました。
ほぼ Java と同じじゃん!
ほかのスクリプト言語Python, JavaScript)は動的型付け言語のため、似たような実装でも Java エンジニアは違和感がありますが、TypeScript は型定義が可能なのでより Java に似ている感があります。

Java と TypeScript のクラス定義の違い

コンストラクタ

まずはコンストラクタ定義の違いです。
Java ではコンストラクタを自クラス名で定義しますが、TypeScript は constructor という名前で定義します。
Java

    Student(int id, String name) {
        this.id = id;
        this.name = name;
    }

TypeScript

    constructor(id:number,name:string){
        this.id = id;
        this.name = name;
    }

ここは Java と TypeScript で違いはあるものの、constructor という直接的な名前で定義可能なので特に戸惑うことはないかと思いました。

アクセス修飾子

Java のクラス定義で注意するのはアクセス制御ですよね。
アクセス制御に使用するアクセス修飾子は実は TypeScript にもあります。
では Java と TypeScript のアクセス修飾子を違いを見ていきます。

アクセス修飾子 Java TypeScript
(なし) 自クラスと同パッケージのクラスからアクセス可能 public と同等
private 自クラスからのみアクセス可能 自クラスからのみアクセス可能
protected 自クラスとサブクラスからアクセス可能 自クラスとサブクラスからアクセス可能
public すべてのクラスからアクセス可能 どこからでもアクセス可能

表を見ればわかるかと思いますが、アクセス修飾子の種類は同じですが、一部意味が異なります。
アクセス修飾子なしは Java ではアクセス制御がかかりますが、TypeScript ではアクセス制御はかかりません。(public と同等)
ですので、最初に紹介した Java と TypeScript のクラス定義は同じように見えて厳密には異なるクラス定義となっています。
Java エンジニア視点では、「修飾子を書かない= package 内プライベート」という前提が無い分、TypeScript の方が初見でも挙動を想像しやすいと感じました。

さいごに

TypeScript のクラス定義は Java エンジニアにとってとっつきやすいものでした。
私が Java 脳で PythonJavaScript を触った時に感じていた違和感、戸惑いは TypeScript ではかなり解消されていると感じています。 今後は interface やクラス継承についても学んで Java との違いをブログにしていきたいと思います。