Article
A Practical Guide to Type-Safe Image Handling in React Native
January 7, 2025

Images are important in all of our React Native projects, and I’d go so far as to say that they are the most common asset that you’ll have to manage in any project you work on.
But working with images in TypeScript can be a pain. Images are retrieved using the require()
method, which means that file names for those images need to be hardcoded in every file that they are used. This makes passing assets into components difficult and refactoring time-consuming and error-prone.
A straightforward way to handle this is to use React Native’s Image.resolveAssetSource()
function. This helper function will return an ImageResolvedAssetSource
that contains the file’s location as well as helpful metadata, such as the image’s width and height. This value is also typed, which makes it more convenient to work with in TypeScript.
Creating a Typed Images Container
Great! Now you’ve got your image as a type-safe asset source, but what if you don’t want to keep having to create these asset sources all over your codebase?
My favorite solution is to create a typed container for all of the images that exist in a given project and have them live alongside each other in one file. This allows me to reference all of the images that I need to, without having to manually import each specific image into the file where it will be used. Instead, I just bring in my images
container.
import { Image } from ‘react-native’;
export const images = {
avatar: Image.resolveAssetSource(require('../assets/images/avatar.png')),
calendar: Image.resolveAssetSource(require('../assets/images/calendar.png')),
laptop: Image.resolveAssetSource(require('../assets/images/laptop.png')),
pencil: Image.resolveAssetSource(require('../assets/images/pencil.png')),
}
Note: If you’d prefer to import this as the default export, you can replace
export const images = { ... }
withexport default { ... }
. Then your import statement would beimport images from '../support/Images'
.
Whenever I need to use one of these images, I import the images
container, and then retrieve the specific file I need:
import images from '../support/Images';
const calendar = images.calendar;
Passing Images into a Component
This works the best in concert with custom components that need to accept an image as a property for the view:
import {
ImageResolvedAssetSource,
StyleSheet,
View,
} from 'react-native';
export function CustomImageDisplay(props: CustomImageDisplayProps): JSX.Element {
return (
<View style={styles.container}>
<Image source={props.image}/>
</View>
);
};
export type CustomImageDisplayProps = {
image: ImageResolvedAssetSource,
}
const styles = StyleSheet.create({
container: {…},
});
In this example component, CustomImageDisplay
takes a prop called image
of type ImageResolvedAssetSource
. Because all of the images in our project are managed in the a container, we can easily import the container and use them like so:
import {
StyleSheet,
View,
} from 'react-native';
import images from '../support/Images';
export function ImageDetailsView(): JSX.Element {
return (
<View style={styles.container}>
<CustomImageDisplay image={images.calendar}/>
</View>
);
};
const styles = StyleSheet.create({
container: {…},
});
Easy-peasy!
Conclusion
Centralizing your images in a single container and making them type-safe with Image.resolveAssetSource()
can help you manage images more cleanly across a React Native project. By handling images this way, you reduce boilerplate, avoid repeated imports, and ensure that the assets you pass around have the correct type information. And on top of all of that, refactoring your images just became that much easier!