sensiolabs/typescript-bundle
SensioLabs TypeScriptBundle integrates the TypeScript compiler into Symfony, helping you compile .ts assets to JavaScript and manage TypeScript sources alongside your application. It provides bundle configuration and tooling for a smoother TypeScript workflow.
Prerequisites. This bundle is an adapter between Symfony's AssetMapper and an external TypeScript transpiler. You need:
symfony/asset-mapper (the bundle does nothing without it),PATH — the bundle is built around Bun (single binary, native TS transpilation, no node_modules needed), with Node/tsc as the usual fallback,tsconfig.json at the project root for editor/type-checking support (the transpiler does not need it, but your IDE does).composer require symfony/asset-mapper # if not already installed
composer require sensiolabs/typescript-bundle
# transpiler — pick one and install it on every machine that builds assets
curl -fsSL https://bun.sh/install | bash
# or: npm i -g typescript
Symfony Flex registers the bundle automatically. If not, add it to config/bundles.php for the all environment.
Where to look first.
sensiolabs/AssetMapperTypeScriptBundle — it is the only authoritative source; the bundle is young and options have shifted between 0.x/1.x releases.php bin/console config:dump-reference typescript — prints the real config tree for the version you installed. Treat any example config (including the ones below) as a starting point and confirm the keys here.php bin/console list typescript — shows the commands the bundle actually registers (typically a build command and a watch/dev variant).php bin/console debug:container --tag=asset_mapper.compiler — confirms the TypeScript compiler is wired into AssetMapper's compilation pipeline.First use case — a typed entry point.
// assets/app.ts
interface Greeting { name: string }
const greet = (g: Greeting): string => `Hello, ${g.name}`;
const el = document.querySelector<HTMLElement>('#app');
if (el) el.textContent = greet({ name: 'TypeScript' });
Compile it (command name depends on version — check list typescript):
php bin/console typescript:build
Then reference the compiled asset exactly as you would a normal AssetMapper asset:
{# templates/base.html.twig #}
{{ importmap('app') }}
Verify what AssetMapper sees before debugging anything else:
php bin/console debug:asset-map | grep app
php bin/console importmap:validate
If app.js (or app.ts) shows up with the expected logical path, the pipeline is wired correctly and any remaining problem is Twig-side.
Build-time compilation, not request-time. The productive pattern is: .ts sources live under assets/, a build step emits .js, and AssetMapper serves the emitted .js. In development, run the bundle's watch command in a second terminal (or under a process manager) so saves are picked up without a manual rebuild. In production, run the build as part of the deploy — before php bin/console asset-map:compile, so the dump contains the compiled JavaScript and not the TypeScript sources.
# deploy pipeline
composer install --no-dev --optimize-autoloader
php bin/console typescript:build # emit .js
php bin/console asset-map:compile # dump into public/assets/
php bin/console cache:warmup
Transpile in the bundle, type-check in CI. Treat the bundle as a transpiler, not a checker. Transpilers strip types and never report type errors. Add a separate, explicit type-check step so red squiggles in the IDE are backed by CI:
// package.json
{
"scripts": {
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"typescript": "^5"
}
}
Run npm run typecheck in CI and locally as a pre-commit hook. This is the single most important workflow habit with this bundle: without it, a typo in a type annotation ships silently.
Import maps for runtime, @types for the editor. AssetMapper resolves bare specifiers through the import map, not through node_modules. So:
php bin/console importmap:require @hotwired/stimulus
# editor-only types, not shipped
npm i -D @types/node @types/webpack-env
Then in TS you can import { Controller } from '@hotwired/stimulus' and both the browser and the type checker are satisfied. Keep these two dependency lists in sync — a very common failure is "works in editor, 404 in browser" because the import-map entry was never added.
Stimulus controllers. The most natural application for this bundle: type your Stimulus controllers.
// assets/controllers/hello_controller.ts
import { Controller } from '@hotwired/stimulus';
export default class extends Controller<HTMLElement> {
static targets = ['output'];
declare readonly outputTarget: HTMLElement;
connect(): void {
this.outputTarget.textContent = 'connected';
}
}
Register the controller directory in assets/controllers.json exactly as you would for .js controllers; the bundle only changes how the source is compiled.
Per-page entry points. Rather than one monolithic app.ts, emit one entry per page (assets/pages/home.ts, assets/pages/checkout.ts) and reference the page-specific one in each template's importmap(). This keeps the import map small and maps cleanly onto Symfony's per-template asset conventions.
tsconfig.json that matches transpiler semantics. Because compilation is per-file, mirror that in your config so the editor and the transpiler agree:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"noEmit": true
},
"include": ["assets/**/*.ts"]
}
isolatedModules: true and verbatimModuleSyntax: true force you to use import type / export type for type-only imports — which is exactly what a per-file transpiler requires.
Cache the transpiler in CI. Bun and Node both download quickly but not instantly. Cache ~/.bun/install/cache (or the Node toolchain) and pin the transpiler version in your CI image so the output is reproducible between local and CI.
Composer script convenience. Wire the build into Composer so nobody forgets it:
{
"scripts": {
"post-install-cmd": ["@typescript-build"],
"typescript-build": "php bin/console typescript:build"
}
}
tsc --noEmit in CI; do not rely on the bundle for correctness.isolatedModules semantics are enforced, not optional. File-by-file transpilation means import { Foo } from './types' where Foo is only a type will break at runtime unless you write import type { Foo }. Same for export type. Enable verbatimModuleSyntax to catch this at type-check time..ts and .js assets. If the build emits .js next to the .ts sources under assets/, AssetMapper will expose both and can complain about conflicting logical paths. Either emit into a dedicated output directory (and point AssetMapper there via framework.asset_mapper.paths), or exclude .ts from the mapper's scan. Check with debug:asset-map — if you see two entries per entry point, this is why.require-dev. If you compile in CI and ship artifacts, you can keep the toolchain off prod — but then your deploy must upload the emitted .js, not just the .ts sources.php bin/console cache:clear (and re-running asset-map:compile) resolves most "it works in dev, 404 in prod" reports..js.map files and whether asset-map:compile copies them; adjust the output directory or disable source maps in prod to avoid shipping them.bun/node must be resolvable on the web/CLI user's PATH, and any config value that names the binary is a plain string — no shell expansion of ~ or environment variables. Test the build command as the same user that runs the web server or the deploy job.tsc compile proves nothing about browser resolution. Whenever you add an npm import in TS, immediately run php bin/console importmap:require <pkg> and importmap:validate; otherwise your typecheck passes and the browser throws Failed to resolve module specifier.config:dump-reference typescript after every upgrade. The package is at 39 stars with zero known dependents — the API surface is small but has moved between releases. Pinning a known-good version in composer.json and reading the changelog before upgrading is cheap insurance.asset_mapper.compiler; decorating or adding your own compiler next to the TypeScript one is the sanctioned way to extend the pipeline without forking the bundle.How can I help you explore Laravel packages today?