A Helm values.yaml grows for two years, gets passed around a Node service as any, and one day someone renames db.host to database.host. Nothing breaks at build time — it breaks at 3 a.m. when the new pod can't reach the database. This converter turns a YAML sample into TypeScript interfaces using quicktype-core, so every nested key is reachable through autocomplete and any rename becomes a compiler error. Useful for Helm values, Kubernetes CRDs, GitHub Actions inputs, application config, and any other YAML that crosses a loader boundary into a typed codebase. Conversion runs entirely in the browser.
How to generate TypeScript from YAML
- Paste a representative YAML sample on the left. The richer the example, the stronger the inferred types — include every optional field at least once.
- Set the root type name (PascalCase).
HelmValues, AppConfig, and WorkflowDef all read well in your IDE. - Click Convert. Anchors and aliases (
&name / *name) are dereferenced first, so the inferred shape matches what your loader will actually return. - Copy the interfaces into a
.d.tsfile or your service'stypes/ directory. Cast at the loader boundary so the rest of the codebase sees a typed object.
Example 1: Application config
A typical Node service config in YAML. Note that retries is a number, features.beta is a boolean, and allowedOrigins is a string array. Quicktype infers each shape from the values.
Input YAML
server:
host: localhost
port: 4000
retries: 3
database:
url: postgres://user:pass@db:5432/app
poolSize: 10
features:
beta: true
experimentalSearch: false
allowedOrigins:
- https://app.example.com
- https://admin.example.com
TypeScript output
export interface AppConfig {
server: Server;
database: Database;
features: Features;
allowedOrigins: string[];
}
export interface Server {
host: string;
port: number;
retries: number;
}
export interface Database {
url: string;
poolSize: number;
}
export interface Features {
beta: boolean;
experimentalSearch: boolean;
}Example 2: Union types from mixed arrays
A GitHub Actions jobs.*.steps entry can be either a run step or a uses step. Include at least one of each branch in the sample so quicktype emits a discriminated union instead of falling back to any.
Input YAML
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: npm ci
name: Install
- run: npm test
name: TestTypeScript output
export interface Workflow {
steps: Step[];
}
export interface Step {
uses?: string;
with?: With;
run?: string;
name?: string;
}
export interface With {
"fetch-depth": number;
}Note how uses, with, run, and name are all marked optional. For a stricter tagged-union shape ({ uses: string } | { run: string }), refine the generated types by hand.
YAML specification reference
Quick reference from YAML 1.2. The converter enforces these rules on input; anything outside them is rejected or normalized.
| Element | Meaning | Example |
|---|
| Mapping | Block or flow key/value pairs | name: alice |
| Sequence | Ordered list with `- ` or flow `[...]` | - apple
- banana |
| Scalar | Plain, single-quoted, double-quoted, literal `|`, folded `>` | "hello\n" |
| Anchor / alias | & defines, * references; enables DRY configs | &base
*base |
| Tag | Explicit type: !!int, !!str, !!binary … | !!int "42" |
Common invalid forms
name:alice // missing space after colon
- item
-indented // inconsistent indent
flag: yes
value: 1 // tab used for indent (YAML forbids tabs)
Why generate types instead of writing them?
Hand-written config types drift the moment someone adds a key to YAML without updating the interface. Generated types stay synchronized because the sample is the source of truth. The compiler immediately flags config.databse.host — a single transposed letter that would otherwise silently return undefined and cascade into a runtime crash. Autocomplete across every nested object is a free side effect, which makes deep configs (Helm values are often 5+ levels deep) navigable for new contributors.
Best practices for high-quality types
- Provide a complete sample. Quicktype infers from values present in the input. A YAML with every optional field filled in produces stronger, less
?-laden types than a minimal one. - Avoid null placeholders.
port: null leaves quicktype with no information. Either comment the line out (so the key is absent) or provide a real value so the type is inferred. - Pick a clear root name. The root type name propagates into every nested interface (
HelmValues generates HelmValuesDatabase etc.). Choose a name you will be happy reading 20 times in your IDE. - Cast at the loader boundary. Wrap your YAML loader once (
function loadConfig(): AppConfig) so the rest of the codebase sees typed objects without per-call as assertions. - Regenerate on schema change. Re-run the converter whenever a new field is added; commit the regenerated
.d.ts alongside the YAML change so reviewers see both at once. - Quote ambiguous scalars. A YAML version like
1.10 parses as the number 1.1 and the inferred type becomes number, not string. Quote it ("1.10") before inferring.
YAML-specific gotchas before type inference
- Anchors and aliases get inlined. A
&defaults block with five aliases produces five copies in the resolved tree. The inferred types reflect the resolved shape, not the DRY source. This is correct but can be surprising if the generated interfaces look bigger than your YAML suggests. - The Norway problem (YAML 1.1 only). Unquoted
NO, YES, ON, OFF are booleans in YAML 1.1, strings in YAML 1.2. This converter uses 1.2, so countryCode: NO stays a string — matching what modern js-yaml, go-yaml, and PyYAML 6+ do. - Bare numeric strings. Port numbers, version tags like
1.10, and ZIP codes parse as numbers and may lose leading or trailing zeros. Quote them to keep the inferred type as string. - Multi-document YAML. Files with
--- separators are parsed as an array of documents. Pass a single document for the cleanest single root type, or wrap the array yourself if your loader returns the multi-doc. - Multi-line scalars (
| / >). These resolve to string like any other string. The fact that the YAML used a block scalar is irrelevant to the type system.
This tool vs other type generators
| Tool | Input | Best for |
|---|
| This converter (quicktype-core) | YAML sample | Quick paste-and-go for app configs |
json-schema-to-typescript | JSON Schema | Strict const literals, unions, patterns — when you already have a schema |
kubernetes-typed / k8s-openapi | K8s OpenAPI | Built-in Kubernetes resources with full schema |
zod with manual schemas | Hand-written | Runtime validation in addition to types |
Sample-driven inference is the fastest path for ad-hoc configs and prototypes. For long-lived public APIs, define a JSON Schema and generate from that — schema-driven types carry richer information (enums, patterns, required vs optional) that no sample can express.
Common use cases
- Typed access to
application.yml loaded by a Node service - Strong types for Helm
values.yaml and chart parameters - Kubernetes CRD spec modeling for controllers and operators
- GitHub Actions / GitLab CI pipeline object modeling for custom actions
- OpenAPI spec stubs when the YAML is hand-authored
Related guides
JSON vs YAML: When to Use What — A Developer's Guide
JSON wins on APIs; YAML wins on configs. Side-by-side syntax, parser behaviour, and where each fits across Kubernetes manifests, REST payloads, and GitHub Actions.
Docker Compose for Beginners: From docker run to YAML
How to translate a pile of docker run commands into a single docker-compose.yml. Service definitions, named networks, volumes, env files, and the override patterns for dev vs prod.
YAML Configuration Files: Syntax, Best Practices, and Common Pitfalls
YAML whitespace is unforgiving and the Norway problem (no:) still bites. Types, anchors, aliases, and the rules that keep Kubernetes manifests, GitHub Actions, and Helm values readable.
Kubernetes Manifest Validation: Catch Errors Before kubectl Apply
The cluster is the most expensive place to discover a YAML typo. Schema validation, server-side dry-run, CRDs, and the apiVersion graveyard.
Frequently Asked Questions
Q: Does it support every YAML feature?
Most YAML 1.2 features via js-yaml. Anchors and aliases are dereferenced to concrete values before type inference, so the resulting interfaces describe the resolved tree. Custom tags from CloudFormation (!Ref, !Sub) are not resolved by the generic parser — render the template with the tool's own CLI first, then infer from the rendered output.
Q: Why are some fields typed as any?
When a field is null or absent in the sample, quicktype has no value to infer from and falls back to any. Provide a richer sample where every optional field has at least one concrete value, or hand-edit the generated interface to refine the type. Empty arrays (tags: []) cause the same problem — include at least one element.
Q: Should I commit the generated types or regenerate at build time?
Commit them. Generated types are part of your public surface — reviewers should see when they change because a config change implies a code change. Build-time generation hides drift inside CI logs and breaks IDE autocomplete on fresh checkouts, which slows onboarding. The .d.ts file should sit next to the YAML it describes.
Q: Can I get types for a JSON Schema instead of a sample?
This converter is sample-driven. For schema-driven generation, pair the YAML Validator or a JSON Schema validator with json-schema-to-typescript — that flow gives you stricter unions, const literals, and pattern types that sample inference cannot recover. The trade-off is needing a maintained schema; for short-lived configs the sample-based approach is faster.
Q: Are union types from mixed-type arrays supported?
Yes. If your sample contains an array with both strings and numbers, the generator emits (string | number)[]. For tagged unions ({ kind: 'a', ... } | { kind: 'b', ... }), include at least one example of each branch in the sample. The generator will produce optional fields rather than a clean discriminated union — refine by hand if you want strict narrowing.
Q: My YAML uses anchors heavily — what happens to the types?
Anchors and aliases are resolved during parsing, so the inferred interfaces describe the expanded tree. Five aliases pointing at &defaults produce five copies of the same nested shape in the inferred types — semantically correct but larger than the YAML. If keeping the DRY structure matters for your codebase, define a named type by hand and reference it from the relevant fields.