#ts-codegen

Minimal TypeScript Telepact example that shows both the generated client and the generated server bindings wired into the runtime library.

Browse the files:

Run it:

make run

The committed generated bindings also type-check in strict modern ESM setups without special compiler concessions:

npm run build
npm run build:bundler

#Source Files

#Makefile

#|
#|  Copyright The Telepact Authors
#|
#|  Licensed under the Apache License, Version 2.0 (the "License");
#|  you may not use this file except in compliance with the License.
#|  You may obtain a copy of the License at
#|
#|  https://www.apache.org/licenses/LICENSE-2.0
#|
#|  Unless required by applicable law or agreed to in writing, software
#|  distributed under the License is distributed on an "AS IS" BASIS,
#|  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#|  See the License for the specific language governing permissions and
#|  limitations under the License.
#|

SHELL := /bin/bash

.PHONY: run dep codegen clean

run: codegen
	@set -euo pipefail; \
	$(MAKE) -C ../../lib/ts; \
	rm -rf node_modules dist telepact.tgz; \
	cp ../../lib/ts/dist-tgz/*.tgz telepact.tgz; \
	npm install --ignore-scripts --no-package-lock; \
	npm run build; \
	npm test

dep:
	@$(MAKE) -C ../../lib/ts

codegen: dep
	@set -euo pipefail; \
	rm -rf ../../sdk/cli/telepact_cli/telepact; \
	cp -r ../../lib/py/telepact ../../sdk/cli/telepact_cli/telepact; \
	rm -rf gen; \
	mkdir -p gen; \
	uv run --project ../../sdk/cli telepact codegen --schema-dir api --lang ts --out gen; \
	: > gen/.license-header-ignore

clean:
	@rm -rf dist node_modules telepact.tgz gen

#api/

#api/greet.telepact.yaml

#|
#|  Copyright The Telepact Authors
#|
#|  Licensed under the Apache License, Version 2.0 (the "License");
#|  you may not use this file except in compliance with the License.
#|  You may obtain a copy of the License at
#|
#|  https://www.apache.org/licenses/LICENSE-2.0
#|
#|  Unless required by applicable law or agreed to in writing, software
#|  distributed under the License is distributed on an "AS IS" BASIS,
#|  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#|  See the License for the specific language governing permissions and
#|  limitations under the License.
#|

- fn.greet:
    subject: "string"
  ->:
    - Ok_:
        message: "string"

#gen/

#gen/genTypes.ts

// Code generated by the Telepact CLI. DO NOT EDIT BY HAND.

import { Message, Client } from 'telepact';
import type { TypedMessage } from 'telepact';

export class TaggedValue_<T, U> {
    tag: T;
    value: U;

    constructor(tag: T, value: U) {
        this.tag = tag;
        this.value = value;
    }
}

export class UntypedTaggedValue_ {
    tag: string;
    value: Record<string, any>;

    constructor(tag: string, value: Record<string, any>) {
        this.tag = tag;
        this.value = value;
    }
}
        

export class GreetInput {
    
    pseudoJson: Record<string, any>;

    constructor(pseudoJson: Record<string, any>) {
        this.pseudoJson = pseudoJson;
    }

    static from({
        subject,
    }: {
        subject: string,
    }): GreetInput {
        const input: Record<string, any> = {};
        input["subject"] = subject;

        return new GreetInput({"fn.greet": input});
    }
    subject(): string {
        return this.pseudoJson["fn.greet"]["subject"];
    }
}

export class GreetOutput {
    
    pseudoJson: Record<string, any>;

    constructor(pseudoJson: Record<string, any>) {
        this.pseudoJson = pseudoJson;
    }
    static Ok_: typeof GreetOutputOk;
    static from_Ok_(payload: {
        message: string,
    }): GreetOutput {
        return new GreetOutput({
            "Ok_": GreetOutputOk.from(payload).pseudoJson
        });
    }

    getTaggedValue():
        TaggedValue_<"Ok_", GreetOutputOk> | TaggedValue_<"NoMatch_", UntypedTaggedValue_> {
        const tag = Object.keys(this.pseudoJson)[0]!;
        if (tag === "Ok_") {
            return new TaggedValue_("Ok_", new GreetOutputOk(this.pseudoJson["Ok_"]));
        }
        return new TaggedValue_("NoMatch_", new UntypedTaggedValue_(tag, this.pseudoJson[tag]));
    }
}

export class GreetOutputOk {
    
    pseudoJson: Record<string, any>;

    constructor(pseudoJson: Record<string, any>) {
        this.pseudoJson = pseudoJson;
    }

    static from({
        message,
    }: {
        message: string,
    }): GreetOutputOk {
        const input: Record<string, any> = {};
        input["message"] = message;

        return new GreetOutputOk(input);
    }
    message(): string {
        return this.pseudoJson["message"];
    }
}
GreetOutput.Ok_ = GreetOutputOk;

export class GreetSelect_ {

    pseudoJson: Record<string, any> = {};

    okmessage(): GreetSelect_ {
        const resultUnion = this.pseudoJson["->"] ?? {};
        const theseFields = resultUnion["Ok_"] ?? [];
        if (!theseFields.includes('message')) {
            theseFields.push('message');
        }
        resultUnion["Ok_"] = theseFields;
        this.pseudoJson["->"] = resultUnion;
        return this;
    }
}

export const greet = {
    Input: GreetInput,
    Output: GreetOutput,
    Select_: GreetSelect_,
} as const;



export class Select_ {
    pseudoJson: Record<string, any> = {};

    constructor(pseudoJson: Record<string, any>) {
        this.pseudoJson = pseudoJson;
    }

    static for_greet(select: GreetSelect_): Select_ {
        return new Select_(select.pseudoJson);
    }
}

export class TypedClient {
    client: Client;

    constructor(client: Client) {
        this.client = client;
    }

    
    async greet(headers: Record<string, any>, input: GreetInput): Promise<TypedMessage<GreetOutput>> {
        const message = await this.client.request(new Message(headers, input.pseudoJson));
        return { headers: message.headers, body: new GreetOutput(message.body)};
    }
}

export class TypedServerHandler {
    
    async greet(_headers: Record<string, any>, _input: GreetInput): Promise<TypedMessage<GreetOutput>> {
        throw new Error('Not implemented');
    }

    functionRoutes(): Record<string, (functionName: string, requestMessage: Message) => Promise<Message>> {
        return {
            
            "fn.greet": async (_functionName: string, requestMessage: Message): Promise<Message> => {
                const argument = requestMessage.body["fn.greet"];
                const { headers: responseHeaders, body } = await this.greet(
                    requestMessage.headers,
                    new GreetInput({ "fn.greet": argument }),
                );
                return new Message(responseHeaders, body.pseudoJson);
            },
        };
    }

    async handler(message: Message): Promise<Message> {
        const functionName = message.getBodyTarget();
        const functionRoute = this.functionRoutes()[functionName];
        if (functionRoute === undefined) {
            throw new Error("Unknown function: " + functionName);
        }
        return await functionRoute(functionName, message);
    }
}

#package.json

{
  "name": "telepact-example-ts-codegen",
  "private": true,
  "scripts": {
    "build": "tsc",
    "build:bundler": "tsc --noEmit --module ESNext --moduleResolution bundler",
    "test": "node --test dist/test_example.js"
  },
  "dependencies": {
    "telepact": "file:telepact.tgz"
  },
  "devDependencies": {
    "@types/node": "^25.0.3",
    "typescript": "^5.9.2"
  },
  "type": "module"
}

#server.ts

//|
//|  Copyright The Telepact Authors
//|
//|  Licensed under the Apache License, Version 2.0 (the "License");
//|  you may not use this file except in compliance with the License.
//|  You may obtain a copy of the License at
//|
//|  https://www.apache.org/licenses/LICENSE-2.0
//|
//|  Unless required by applicable law or agreed to in writing, software
//|  distributed under the License is distributed on an "AS IS" BASIS,
//|  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//|  See the License for the specific language governing permissions and
//|  limitations under the License.
//|

import { createServer, IncomingMessage, Server as HttpServer, ServerResponse } from 'node:http';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { FunctionRouter, Server, ServerOptions, TelepactSchema, TelepactSchemaFiles } from 'telepact';
import type { Response, TypedMessage } from 'telepact';
import { GreetInput, GreetOutput, TypedServerHandler, greet } from './gen/genTypes.js';

const files = new TelepactSchemaFiles('api', fs, path);
const schema = TelepactSchema.fromFileJsonMap(files.filenamesToJson);
const options = new ServerOptions();
options.authRequired = false;

class GreetingHandler extends TypedServerHandler {
    async greet(_headers: Record<string, any>, input: GreetInput): Promise<TypedMessage<GreetOutput>> {
        return {
            headers: {},
            body: greet.Output.from_Ok_({
                message: `Hello ${input.subject()} from generated code!`,
            }),
        };
    }
}

const functionRouter = new FunctionRouter(new GreetingHandler().functionRoutes());
const telepactServer = new Server(schema, functionRouter, options);

function readRequestBytes(request: IncomingMessage): Promise<Uint8Array> {
    return new Promise((resolve, reject) => {
        const chunks: Buffer[] = [];
        request.on('data', (chunk) => {
            chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
        });
        request.on('end', () => resolve(new Uint8Array(Buffer.concat(chunks))));
        request.on('error', reject);
    });
}

function writeTelepactResponse(responseWriter: ServerResponse, response: Response): void {
    responseWriter.statusCode = 200;
    responseWriter.setHeader('Content-Type', '@bin_' in response.headers ? 'application/octet-stream' : 'application/json');
    responseWriter.end(Buffer.from(response.bytes));
}

export function createHttpServer(): HttpServer {
    return createServer((request, responseWriter) => {
        void (async () => {
            if (request.method !== 'POST' || request.url !== '/api/telepact') {
                responseWriter.statusCode = 404;
                responseWriter.end();
                return;
            }

            const requestBytes = await readRequestBytes(request);
            const response = await telepactServer.process(requestBytes);
            writeTelepactResponse(responseWriter, response);
        })().catch((error: unknown) => {
            responseWriter.statusCode = 500;
            responseWriter.end(String(error));
        });
    });
}

#test_example.ts

//|
//|  Copyright The Telepact Authors
//|
//|  Licensed under the Apache License, Version 2.0 (the "License");
//|  you may not use this file except in compliance with the License.
//|  You may obtain a copy of the License at
//|
//|  https://www.apache.org/licenses/LICENSE-2.0
//|
//|  Unless required by applicable law or agreed to in writing, software
//|  distributed under the License is distributed on an "AS IS" BASIS,
//|  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//|  See the License for the specific language governing permissions and
//|  limitations under the License.
//|

import assert from 'node:assert/strict';
import test from 'node:test';
import type { AddressInfo } from 'node:net';
import { Client, ClientOptions, Message, Serializer } from 'telepact';
import { TypedClient, greet } from './gen/genTypes.js';
import { createHttpServer } from './server.js';

async function runServer(server: ReturnType<typeof createHttpServer>): Promise<void> {
    await new Promise<void>((resolve) => {
        server.listen(0, '127.0.0.1', () => resolve());
    });
}

async function stopServer(server: ReturnType<typeof createHttpServer>): Promise<void> {
    await new Promise<void>((resolve, reject) => {
        server.close((error) => {
            if (error) {
                reject(error);
                return;
            }
            resolve();
        });
    });
}

async function postBytes(url: string, requestBytes: Uint8Array): Promise<Uint8Array> {
    const response = await fetch(url, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: Buffer.from(requestBytes),
    });
    return new Uint8Array(await response.arrayBuffer());
}

test('codegen example runs end to end', async () => {
    const server = createHttpServer();
    await runServer(server);
    try {
        const address = server.address() as AddressInfo;
        const url = `http://127.0.0.1:${address.port}/api/telepact`;

        const adapter = async (message: Message, serializer: Serializer): Promise<Message> => {
            const requestBytes = serializer.serialize(message);
            const responseBytes = await postBytes(url, requestBytes);
            return serializer.deserialize(responseBytes);
        };

        const client = new TypedClient(new Client(adapter, new ClientOptions()));
        const response = await client.greet({}, greet.Input.from({ subject: 'Telepact' }));

        const ok = response.body.getTaggedValue();
        assert.equal(ok.tag, 'Ok_');
        assert.equal(ok.value.message(), 'Hello Telepact from generated code!');
        assert.deepEqual(response.headers, {});
    } finally {
        await stopServer(server);
    }
});

#tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": [
      "ES2022",
      "DOM"
    ],
    "rootDir": ".",
    "outDir": "dist",
    "strict": true,
    "verbatimModuleSyntax": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": [
    "*.ts",
    "gen/**/*.ts"
  ]
}