Note: TypeScript templates are an experimental feature. To enable it, set the environment variable
NELM_FEAT_TYPESCRIPT=true.
Overview
In addition to Helm templates, werf can generate Kubernetes manifests with TypeScript. Helm templates and TypeScript templates can coexist in the same chart — resulting manifests are merged into a single multi-doc YAML document.
TypeScript templates work out of the box: deploying a chart that contains a ts/ directory requires no additional tools or configuration — werf automatically downloads the Deno TypeScript runtime and renders the TypeScript templates.
Why TypeScript
Helm’s templating language works well for simple cases but becomes hard to maintain as chart complexity grows: primitive language with lots of gotchas, limited library, performance issues, debug difficulties, poor IDE/editor support and so on. TypeScript in werf solves these problems without complicating the deployment workflow.
Features
- IDE support — full autocompletion, type checking, go-to-definition, and refactoring in any editor with Deno/TypeScript support (VS Code, JetBrains, Neovim, etc.).
- Standard syntax — proper functions, loops, and conditionals instead of awkward template engine constructs.
- Pure TypeScript —
tsdirectory is a regular Deno TypeScript project, and can be render without werf, with just Deno TypeScript runtime. - Large ecosystem — TypeScript is one of the most popular languages with extensive documentation, community resources, and tooling.
- Almost any third-party TypeScript/JavaScript library can be used, for example kubernetes-models, cdk8s or any other library from npm/Deno ecosystems.
- Testing — test your code using common TypeScript libraries and tooling.
- No extra host requirements — to deploy a TS chart all you need is werf. No need to install Node, Deno, npm, npm modules or anything else. We handle all of this for you, just do a
werf converge. - Isolated environments — npm modules are bundled into the chart by default, and the Deno runtime can be provided by the host system, so no network calls will be done during the deployment, except to the Kubernetes itself.
- Security — code runs in an isolated Deno sandbox with no access to the network, environment variables, or process execution. Filesystem access is limited to reading chart files.
Quick start
Initialize TypeScript files in an existing chart:
werf chart ts init
It will bootstrap the .helm/ts/ directory, which contains a TypeScript project skeleton and a few files with sample resources. Try modifying ts/src/deployment.ts — for example, change the number of replicas — then check the result:
werf render --dev
To deploy:
werf converge --dev
Chart structure
apiVersion: v2
name: ts-chart-example
version: 0.1.0
{
"tasks": {
"build": {
"description": "Run deno build",
"command": "deno bundle --output=dist/bundle.js src/index.ts"
},
"dev": {
"description": "Run in development mode",
"command": "deno run --no-remote --deny-read --deny-write --deny-net --deny-env --deny-run --allow-read=input.example.yaml src/index.ts --input-file ./input.example.yaml"
},
"start": {
"description": "Run the bundled dist/bundle.js",
"command": "deno run --no-remote --deny-read --deny-write --deny-net --deny-env --deny-run --allow-read=input.example.yaml dist/bundle.js --input-file ./input.example.yaml"
}
},
"imports": {
"@nelm/chart-ts-sdk": "npm:@nelm/chart-ts-sdk@^0.1.5"
}
}
{
"version": "5",
"specifiers": {
"npm:@nelm/chart-ts-sdk@~0.1.4": "0.1.4"
},
"npm": {
"@jsr/std__yaml@1.0.12": {
"integrity": "sha512-pz/BisWZWH16JvLJBwrNwUwfIsRnf9qniMrmI6Z3vIAcVRVFcA5+i4o6z6QqsMKqFzjlB66WZE+jSyujT/RvRg==",
"tarball": "https://npm.jsr.io/~/11/@jsr/std__yaml/1.0.12.tgz"
},
"@nelm/chart-ts-sdk@0.1.4": {
"integrity": "sha512-NCeflvAuZQxzmGZGpm0lP3Uy5d2xYRq8TKW0MWTKyknnZAe2tSv9+2uCZVA46oIxdm9FsBKocmCHEfqyOwe4lQ==",
"dependencies": [
"@std/yaml@npm:@jsr/std__yaml@1.0.12"
]
}
},
"workspace": {
"dependencies": [
"npm:@nelm/chart-ts-sdk@~0.1.4"
]
}
}
Capabilities:
APIVersions:
- v1
HelmVersion:
go_version: go1.25.0
version: v3.20
KubeVersion:
Major: "1"
Minor: "35"
Version: v1.35.0
Chart:
APIVersion: v2
Annotations:
anno: value
AppVersion: 1.0.0
Condition: ts-chart-example.enabled
Description: ts-chart-example description
Home: https://example.org/home
Icon: https://example.org/icon
Keywords:
- ts-chart-example
Maintainers:
- Email: john@example.com
Name: john
URL: https://example.com/john
Name: ts-chart-example
Sources:
- https://example.org/ts-chart-example
Tags: ts-chart-example
Type: application
Version: 0.1.0
Files:
myfile: "content"
Release:
IsInstall: false
IsUpgrade: true
Name: ts-chart-example
Namespace: ts-chart-example
Revision: 2
Service: Helm
Values:
global:
werf:
name: myapp
version: v2.35.0
repo: example.org/mycompany/myapp
env: production
images:
app:
registry: example.org
namespace: mycompany
name: myapp
tag: a1b2c3d4-1234567890
digest: "sha256:abcdef1234567890"
tag_digest: "a1b2c3d4-1234567890@sha256:abcdef1234567890"
image: example.org/mycompany/myapp
repository: mycompany/myapp
ref: "example.org/mycompany/myapp:a1b2c3d4-1234567890@sha256:abcdef1234567890"
ref_tag: "example.org/mycompany/myapp:a1b2c3d4-1234567890"
repository_ref: "mycompany/myapp:a1b2c3d4-1234567890@sha256:abcdef1234567890"
repository_tag: "mycompany/myapp:a1b2c3d4-1234567890"
name_ref: "myapp:a1b2c3d4-1234567890@sha256:abcdef1234567890"
name_tag: "myapp:a1b2c3d4-1234567890"
commit:
date:
human: "2025-01-15 12:00:00 +0000"
unix: 1736942400
hash: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
image:
repository: nginx
tag: latest
replicaCount: 1
service:
enabled: true
port: 80
type: ClusterIP
import type { WerfRenderContext } from '@nelm/chart-ts-sdk';
import { getFullname, getLabels, getSelectorLabels } from './helpers.ts';
export function newDeployment($: WerfRenderContext): object {
const name = getFullname($);
return {
apiVersion: 'apps/v1',
kind: 'Deployment',
metadata: {
name,
labels: getLabels($),
},
spec: {
replicas: $.Values.replicaCount ?? 1,
selector: {
matchLabels: getSelectorLabels($),
},
template: {
metadata: {
labels: getSelectorLabels($),
},
spec: {
containers: [
{
name: name,
image: ($.Values.image?.repository ?? 'nginx') + ':' + ($.Values.image?.tag ?? 'latest'),
ports: [
{
name: 'http',
containerPort: $.Values.service?.port ?? 80,
},
],
},
],
},
},
},
};
}
import type { WerfRenderContext } from '@nelm/chart-ts-sdk';
/**
* Truncate string to max length, removing trailing hyphens.
*/
export function trunc(str: string, max: number): string {
if (str.length <= max) return str;
return str.slice(0, max).replace(/-+$/, '');
}
/**
* Get the fully qualified app name.
* Truncated at 63 chars (DNS naming spec limit).
*/
export function getFullname($: WerfRenderContext): string {
if ($.Values.fullnameOverride) {
return trunc($.Values.fullnameOverride, 63);
}
const chartName = $.Values.nameOverride || $.Chart.Name;
if ($.Release.Name.includes(chartName)) {
return trunc($.Release.Name, 63);
}
return trunc(`${$.Release.Name}-${chartName}`, 63);
}
export function getLabels($: WerfRenderContext): Record<string, string> {
return {
'app.kubernetes.io/name': $.Chart.Name,
'app.kubernetes.io/instance': $.Release.Name,
};
}
export function getSelectorLabels($: WerfRenderContext): Record<string, string> {
return {
'app.kubernetes.io/name': $.Chart.Name,
'app.kubernetes.io/instance': $.Release.Name,
};
}
import { WerfRenderContext, RenderResult, render } from '@nelm/chart-ts-sdk';
import { newDeployment } from './deployment.ts';
import { newService } from './service.ts';
function generate($: WerfRenderContext): RenderResult {
const manifests: object[] = [];
manifests.push(newDeployment($));
if ($.Values.service?.enabled !== false) {
manifests.push(newService($));
}
return { manifests };
}
await render(generate);
import type { WerfRenderContext } from '@nelm/chart-ts-sdk';
import { getFullname, getLabels, getSelectorLabels } from './helpers.ts';
export function newService($: WerfRenderContext): object {
return {
apiVersion: 'v1',
kind: 'Service',
metadata: {
name: getFullname($),
labels: getLabels($),
},
spec: {
type: $.Values.service?.type ?? 'ClusterIP',
ports: [
{
port: $.Values.service?.port ?? 80,
targetPort: 'http',
},
],
selector: getSelectorLabels($),
},
};
}
replicaCount: 1
image:
repository: nginx
tag: latest
service:
enabled: true
type: ClusterIP
port: 80
Developing a chart with TypeScript templates
Install Deno and follow the setup guide for your IDE/editor (VS Code, JetBrains, Neovim, etc.).
Initialize TypeScript files in the chart if not already initialized:
werf chart ts init
Open the ts/ directory in your editor as a regular Deno/TypeScript project. You can work with it the same way you would with any TypeScript codebase — run scripts, write tests, use a debugger. Deno provides a rich set of tools for testing, linting, formatting, and more. See Deno documentation for details.
The codebase can be organized as you wish. The only requirement is that ts/src/index.ts exists, and render function from @nelm/chart-ts-sdk must be called. Otherwise, no TypeScript rendering happens.
To debug templates rendering in an environment that is very close to how werf runs Deno, you can use dev task from ts/deno.json:
cd .helm/ts
deno task dev
TypeScript engine will call render function from ts/src/index.ts with the example context from ts/input.example.yaml. The resulting YAML will be printed to the console below the Rendered manifests: message.
Install libraries using deno add, for example, try to install kubernetes-models — library for strict Kubernetes resource typing:
deno add npm:kubernetes-models
The dependency is added to deno.json automatically. Now you can import and use it:
// .helm/ts/src/deployment.ts:
import { Deployment } from 'kubernetes-models/apps/v1';
export function newDeployment($: WerfRenderContext): object {
return new Deployment({
metadata: { name: 'myapp' },
spec: {
// other fields
},
}).toJSON();
}
To ensure that everything actually works with the werf deno runtime, run:
werf lint --dev
werf render --dev
How to deploy a chart with TypeScript templates
Simply run werf converge: the Deno binary will be downloaded into the cache and TypeScript templates will be rendered and deployed.
Note: According to giterminism policies, all changed files must be committed.
Deploying into isolated environments
For the isolated environments, where Deno cannot be downloaded automatically:
- Publish the chart:
werf bundle publish --repo example.org/mycompany/myappAll npm modules will be minified and bundled inside, so that the chart can be installed even without Internet access.
- On the target machine with an isolated environment (no network access), download Deno manually and run:
werf bundle apply --repo example.org/mycompany/myapp --deno-binary-path /usr/local/bin/denoWhere
/usr/local/bin/denois the path to the local Deno binary. TypeScript templates will be rendered and deployed using pre-compiled files from the chart bundle.
SDK API overview
TypeScript engine uses the @nelm/chart-ts-sdk package.
“render” and “generate” functions
index.ts must call the render() function. The function generate(), which will actually generate the manifests, should be passed to the render() function as an argument, for example:
// .helm/ts/src/index.ts:
await render(generate);
“WerfRenderContext” object
The generate function receives the root context in the $ variable of type WerfRenderContext — the same context as in Helm templates:
| Field | Type | Description |
|---|---|---|
$.Values |
WerfServiceValues |
Chart parameters + service values at $.Values.global.werf |
$.Release |
Release |
Release information |
$.Chart |
ChartMetadata |
Metadata from Chart.yaml |
$.Capabilities |
Capabilities |
Cluster capabilities (API versions, Kubernetes version) |
$.Files |
Record<string, Uint8Array> |
Raw chart files (except templates/ and ts/) |
See the example context in ts/input.example.yaml. For details on parameters and how they are constructed, see Parametrize templates.
“RenderResult” object
The generate function returns RenderResult — an object with a manifests array. Each element is a plain JavaScript object representing a Kubernetes resource. Example output:
{
"manifests": [
{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": { "name": "myapp" },
"spec": { "..." }
},
{
"apiVersion": "v1",
"kind": "Service",
"metadata": { "name": "myapp" },
"spec": { "..." }
}
]
}
Each object is serialized to YAML and included in the final rendered output.