Get rid of all string-based asset references in React Native — forever.
rn-typed-assets scans your asset directories and generates a typed TypeScript registry, so that every image, SVG, and Lottie animation is accessed through a named constant instead of a brittle require('../../../assets/icon.png') path. If the file doesn't exist, the import fails at generation time — not at runtime.
// Before
<Image source={require('../../../assets/toast/info.png')} />
<LottieView source={require('../../utils/loading.json')} />
// After
import { Assets, Lotties } from '../generated/assets.gen';
<Image source={Assets.toast.info} />
<LottieView source={Lotties.loading} />This tool is directly inspired by two codegen tools from other ecosystems:
-
SwiftGen — the canonical Swift code generator for Xcode resources. SwiftGen pioneered the pattern of scanning asset catalogs and emitting fully type-safe Swift enums, eliminating string-based
UIImage(named:)calls entirely. -
FlutterGen — the Flutter equivalent. FlutterGen reads
pubspec.yamlassets and emits Dart classes with typed getters, soAssets.images.profile.image()replacesImage.asset('assets/images/profile.jpg').
rn-typed-assets brings the same discipline to React Native: a manifest-driven, deterministic generator that makes unused or mistyped asset references a compile-time (or generation-time) problem rather than a runtime crash.
- Zero-config for standard RN projects — works out of the box with the default
src/assetslayout - Three built-in asset types —
image(PNG/JPG/WebP),svg,lottie(JSON) - Deterministic, sorted output — generated files are stable across runs and friendly to code review
- Collision detection — files that normalize to the same key (e.g.
harini-cry.pngandharini_cry.png) are caught at generation time with a clear error - Manifest-backed audit — find and optionally delete unused assets that are no longer referenced anywhere in source
- Automatic source rewriting —
generate --inplaceandorganizerewrite everyrequire()call, ES moduleimportstatement, and stale dotted reference in your source files to match the regenerated manifest - Content-hash diffing — each manifest entry now carries a SHA-1 hash of the file's bytes, enabling the codemod to track files that move or are renamed without content changes
- Asset organization —
organizemigrates flat or legacy asset directories into canonical subdirectories (images/,svg/,lottie/) in one command - Configurable — override paths, export names, TypeScript type imports, or add entirely new asset types via
rn-typed-assets.config.js - Programmatic API — every function is exported; integrate the generator into your own scripts or build tools
- Lightweight — zero runtime dependencies;
typescriptis a peer dependency used only by the audit command
src/assets/ commands/
toast/info.png ──┐ generate
toast/warning.png ──┤ scan ──► assets.gen.ts (typed require() registry)
lottie/loading.json─┤ assets.manifest.json (path ↔ key + contentHash)
svg/logo.svg ───────┘
generate --inplace
src/**/*.{ts,tsx,js,jsx} ◄────── rewrite require() → Assets.*
migrate import X → Assets.*
update stale dotted references
organize src/assets
svg/logo.svg ───────► svg/logo.svg (move to canonical dir)
→ regenerate + rewrite sources
audit
src/**/*.{ts,tsx,js,jsx} ──────► compare usages in source
vs. entries in manifest
→ report (or delete) unused assets
- Scan — For each enabled asset type, recursively list files under the configured
rootDir. - Normalize — Convert each filename to a stable camelCase key (
harini-cry.png→hariniCry,1.png→n1). - Build registry tree — Assemble a nested object tree from path segments. Detect and resolve branch/leaf collisions automatically (e.g. a file named
point.pngalongside apoint/directory becomespointAsset). - Emit — Write
assets.gen.ts(a typedas constobject) andassets.manifest.json(a stable index of every key ↔ file mapping).
- Parse source files — Use the TypeScript Compiler API to walk the AST of every
.ts/.tsx/.js/.jsxfile undersourceRoots. - Collect usages — Detect
Assets.*,Lotties.*, andSvgs.*property-access chains. Also collect legacyrequire('../assets/...')calls for files still in migration. - Compare against manifest — Any key present in the manifest but unreferenced in source is reported as unused. Any key used in source but absent from the manifest is reported as unknown.
- Optionally fix — With
--fix, delete the unused files and regenerate the manifest.
npm install --save-dev rn-typed-assetstypescript must be available in the project (it is a peerDependency). Most React Native projects already have it as a dev dependency.
{
"scripts": {
"assets:generate": "rn-typed-assets generate",
"assets:generate:inplace": "rn-typed-assets generate --inplace",
"assets:organize": "rn-typed-assets organize src/assets",
"assets:audit": "rn-typed-assets audit",
"assets:audit:fix": "rn-typed-assets audit --fix"
}
}npm run assets:generateThis writes two files to src/generated/:
assets.gen.ts— the typed registry you import in your componentsassets.manifest.json— the index used by the audit command (commit this file)
import { Assets, Lotties, Svgs } from './generated/assets.gen';
// Images
<Image source={Assets.toast.info} />
<Image source={Assets.coupang.hariniCry} />
// Lottie
<LottieView source={Lotties.loading} autoPlay loop />
// SVG — see "SVG type configuration" below for usage patterns
const logo = Svgs.logo;npm run assets:audit # report unused entries
npm run assets:audit -- --fix # delete unused files and regeneratern-typed-assets <command> [options]These flags work with every command (generate, audit, organize):
| Flag | Description | Default |
|---|---|---|
--case <camel|snake> |
Casing style for generated asset keys | camel |
--on-collision <error|first> |
When two files normalize to the same key: error fails the run, first keeps the first sorted path and drops the rest |
error |
--output <text|json> |
Output format. json prints exactly one machine-readable envelope to stdout and suppresses all other logs |
text |
--root <path> |
Project root directory | cwd |
--config <path> |
Path to config file | ./rn-typed-assets.config.js |
CLI flags override the equivalent keyCase / onCollision settings in your config file.
With --output json, every command emits exactly one JSON envelope to stdout and writes nothing else there. The process exit code still signals success (0) or failure (1), so callers can read either the stream or the code.
{ "ok": true, "command": "generate", "data": { "...": "..." }, "error": null }On failure (ok: false, non-zero exit) data is null and error.message describes the problem:
{ "ok": false, "command": "generate", "data": null, "error": { "message": "..." } }The data shape is the per-command contract:
| Command | data fields |
|---|---|
generate |
types: string[], count: number, rewrittenFiles: number, collisions: Collision[], entries: { type, keyPath, filePath, modulePath }[] |
audit |
types: string[], manifestMatchesFilesystem: boolean, unknownGeneratedUsages: string[], unusedEntries: string[], deletedFiles?: string[] (with --fix) |
organize |
types: string[], count: number, movedFiles: string[], rewrittenFiles: number, collisions: Collision[] |
A Collision is { type, keyPath, kept, dropped } and is only populated under --on-collision first (in text mode these surface as Collision: lines). Note that audit reports ok: false and exits 1 when the manifest is stale or unknown generated usages are detected; unused entries alone are reported with ok: true.
rn-typed-assets generate --types=image --output json
rn-typed-assets generate --case snake # snake_case keys
rn-typed-assets generate --on-collision first # keep first path, drop duplicatesKeep casing consistent across commands.
generateandauditmust use the samekeyCase, orauditwill regenerate keys in a different casing and report the manifest as stale. Prefer settingkeyCasein the config file (both commands read it) over passing--caseto only one command.
Scan asset directories and emit assets.gen.ts + assets.manifest.json.
| Flag | Description | Default |
|---|---|---|
--types <types> |
Comma-separated list of asset types to include | image,svg,lottie |
--inplace |
Rewrite source files to replace stale references after regen | false |
--root <path> |
Project root directory | cwd |
--config <path> |
Path to config file | ./rn-typed-assets.config.js |
rn-typed-assets generate
rn-typed-assets generate --inplace # rewrite sources after regen
rn-typed-assets generate --types=image,lottie
rn-typed-assets generate --root=/path/to/projectWhen --inplace is set, the tool loads the previous manifest before writing the new one, diffs them by content hash, and rewrites every source file that contains a stale require() path, an ES module import statement for an asset file, or a renamed dotted symbol reference.
ES module imports are fully migrated: the import declaration is removed, all usages of the imported binding are replaced with the generated symbol, and the import { … } from '…/assets.gen' statement is added or extended automatically.
// Before
import logo from '../assets/logo.png';
const el = <Image source={logo} />;
// After (generated by --inplace)
import { Assets } from '../generated/assets.gen';
const el = <Image source={Assets.logo} />;Move asset files from legacy or flat directories into canonical subdirectories, then regenerate and rewrite sources.
rn-typed-assets organize <assetsDir>| Argument | Description |
|---|---|
<assetsDir> |
Path to the asset root to organize (relative to cwd) |
| Flag | Description | Default |
|---|---|---|
--types <types> |
Asset types to move | image,svg,lottie |
--root <path> |
Project root directory | cwd |
--config <path> |
Path to config file | ./rn-typed-assets.config.js |
rn-typed-assets organize src/assetsCanonical subdirectory layout after organize:
src/assets/
images/ ← PNG/JPG/WebP
svg/ ← SVG
lottie/ ← Lottie JSON
Files already in the canonical location are left untouched. Source files with require() calls pointing to the old paths are rewritten automatically.
Compare manifest against actual source-file usages.
| Flag | Description | Default |
|---|---|---|
--types <types> |
Asset types to include in the audit | image,svg,lottie |
--fix |
Delete unused asset files and regenerate the manifest | false |
--root <path> |
Project root directory | cwd |
--config <path> |
Path to config file | ./rn-typed-assets.config.js |
rn-typed-assets audit
rn-typed-assets audit --fix
rn-typed-assets audit --types=imageCreate rn-typed-assets.config.js in your project root to override any default. The file is optional — omitting it is equivalent to accepting all defaults.
// rn-typed-assets.config.js
module.exports = {
// Where to write assets.gen.ts and assets.manifest.json
// Default: 'src/generated'
outputDir: 'src/generated',
// Directories and entry files scanned by the audit command
// Default: ['src', 'App.tsx', 'index.js']
sourceRoots: ['src', 'App.tsx', 'index.js'],
// Casing style for generated asset keys: 'camel' | 'snake'
// Default: 'camel' (e.g. 'home-button.png' -> homeButton vs. home_button)
keyCase: 'camel',
// Collision policy when two files normalize to the same key: 'error' | 'first'
// 'error' fails the run; 'first' keeps the first sorted path and drops the rest.
// Default: 'error'
onCollision: 'error',
// Per-type configuration (all fields are optional overrides)
types: {
image: {
rootDir: 'src/assets', // scan root
extensions: ['.png', '.jpg', '.jpeg', '.webp'], // included extensions
exportName: 'Assets', // export const Assets = ...
typeImport: {
typeName: 'ImageRequireSource', // TypeScript type name to import
from: 'react-native', // import source
},
// valueType: 'React.FC<ImageProps>', // annotation applied to values (defaults to typeImport.typeName)
},
svg: {
rootDir: 'src/assets/svg',
extensions: ['.svg'],
exportName: 'Svgs',
// SVG type depends on your setup — see "SVG type configuration" section
inlineType: 'unknown',
},
lottie: {
rootDir: 'src/assets/lottie',
extensions: ['.json'],
exportName: 'Lotties',
typeImport: {
typeName: 'AnimationObject',
from: 'lottie-react-native',
},
},
},
};SVG files behave differently depending on your Metro setup. The default inlineType: 'unknown' is intentionally conservative — override it in your config to match your project.
Pattern A — with react-native-svg-transformer
require('./logo.svg') returns a React component. Configure a real type import and a valueType:
// rn-typed-assets.config.js
module.exports = {
types: {
svg: {
typeImport: { typeName: 'SvgProps', from: 'react-native-svg' },
valueType: 'React.FC<SvgProps>',
},
},
};Usage:
const Logo = Svgs.splash.logo; // React.FC<SvgProps>
<Logo width={24} height={24} />;Pattern B — without transformer, using SvgUri
require('./logo.svg') returns a number (Metro asset ID), just like images. You must resolve it to a URI string before passing to SvgUri.uri:
// rn-typed-assets.config.js
module.exports = {
types: {
svg: {
typeImport: { typeName: 'ImageRequireSource', from: 'react-native' },
},
},
};Usage:
import { Asset } from 'expo-asset'; // Expo
const uri = Asset.fromModule(Svgs.splash.logo).uri;
<SvgUri uri={uri} width={24} height={24} />;Note:
<SvgUri uri={Svgs.splash.logo} />will not work directly —SvgUri.uriisstring | null, butrequire()without transformer returnsnumber.
Any type not in the defaults can be added under types. The audit command discovers it automatically via the exportName → type reverse map.
module.exports = {
types: {
font: {
rootDir: 'src/assets/fonts',
extensions: ['.ttf', '.otf'],
exportName: 'Fonts',
inlineType: 'string', // emits: export type FontsAssetSource = string
},
},
};Given this asset tree:
src/assets/
toast/
info.png
warning.png
coupang/
harini-cry.png
lottie/
loading.json
svg/
logo.svg
Running rn-typed-assets generate produces:
// src/generated/assets.gen.ts
/* eslint-disable @typescript-eslint/no-require-imports -- require() is intentional for React Native static asset bundling */
// Auto-generated by rn-typed-assets. Do not edit manually.
import type { ImageRequireSource } from 'react-native';
import type { AnimationObject } from 'lottie-react-native';
export type SvgsAssetSource = unknown;
export const Assets = {
coupang: {
hariniCry:
require('../assets/coupang/harini-cry.png') as ImageRequireSource,
},
toast: {
info: require('../assets/toast/info.png') as ImageRequireSource,
warning: require('../assets/toast/warning.png') as ImageRequireSource,
},
} as const;
export const Lotties = {
loading: require('../assets/lottie/loading.json') as AnimationObject,
} as const;
export const Svgs = {
logo: require('../assets/svg/logo.svg') as SvgsAssetSource,
} as const;| Filename | keyCase: 'camel' (default) |
keyCase: 'snake' |
|---|---|---|
harini-cry.png |
hariniCry |
harini_cry |
camera_guide.png |
cameraGuide |
camera_guide |
Info-Filled.png |
infoFilled |
info_filled |
1.png |
n1 (numeric prefix → n) |
n_1 |
point.png alongside point/ dir |
pointAsset (leaf/branch collision) |
point_asset |
Set the casing via the keyCase config option or the --case <camel\|snake> CLI flag.
All functions are available for use in custom build scripts:
const {
loadConfig,
collectAssetEntries,
generateAssetsModule,
generateAssetsManifest,
writeGeneratedAssets,
auditAssetUsage,
collectGeneratedAssetUsages,
// Codemod
collectAssetImportBindings,
diffAssetManifests,
rewriteTypedAssetSource,
} = require('rn-typed-assets');
const config = loadConfig(projectRoot);
// Generate
const { entries, moduleContent, manifest } = writeGeneratedAssets({
projectRoot,
types: ['image', 'lottie'],
config,
});
// Audit (requires typescript peer dep)
const usages = collectGeneratedAssetUsages(sourceCode, filePath, config);
const report = auditAssetUsage({
manifest,
generatedUsages: usages,
requirePaths: [],
config,
});
// Codemod — rewrite a source file after a manifest change
const { renamedSymbols, currentSymbolsByFilePath } = diffAssetManifests({
previousManifest,
nextManifest,
config,
});
const { changed, code } = rewriteTypedAssetSource({
code: fs.readFileSync(filePath, 'utf8'),
filePath, // relative to projectRoot
previousManifest,
nextManifest,
projectRoot,
config,
});
if (changed) {
fs.writeFileSync(filePath, code);
}See src/index.js for the full list of exported functions.
Add generation and audit to your CI pipeline to catch drift between the manifest and the filesystem:
# .github/workflows/ci.yml
- name: Verify asset manifest is up to date
run: |
npm run assets:generate
git diff --exit-code src/generated/Or use a pre-commit hook via Husky:
# .husky/pre-commit
npm run assets:generate
git add src/generated/assets.gen.ts src/generated/assets.manifest.jsonAutomatically re-run generate when files under any rootDir are added, removed, or renamed. Useful during active development without having to manually invoke the CLI.
rn-typed-assets generate --watch # plannedA Metro resolver plugin that runs generation as part of the bundler's startup, ensuring assets.gen.ts is always in sync before the first bundle is produced — eliminating the need for a separate pre-build step.
A first-class init command that scaffolds a Husky pre-commit hook and lint-staged configuration so that generation runs automatically whenever asset files are staged.
rn-typed-assets init --hooks # plannedExtend the default type set to cover fonts (.ttf, .otf) and audio files (.mp3, .wav, .aac) with appropriate TypeScript types (FontSource, NodeRequire). These types already work today via the custom-type config API; the plan is to promote them to built-in defaults.
A --strict flag for the audit command that exits non-zero if the manifest is stale, if any unknown keys are in use, or if any unused entries remain — making the full audit a one-command CI gate.
rn-typed-assets audit --strict # plannedAllow multiple packages in a workspace to share a single invocation, with per-package output directories and per-package manifests, controlled by a root-level config or workspace glob.
Inspired by SwiftGen's Stencil templates, allow users to supply a custom EJS or Handlebars template for assets.gen.ts, enabling alternative output styles — for example, emitting functions instead of require() literals, or targeting a custom asset loader.
MIT