Expo File System Class API Migration
Problem
After upgrading to Expo SDK 52+ (expo-file-system 18+), the legacy function-based API
no longer works. Code using FileSystem.deleteAsync(), FileSystem.getInfoAsync(),
FileSystem.cacheDirectory, etc. will fail with TypeScript errors or runtime issues.
Context / Trigger Conditions
You'll encounter this when:
- •Upgrading Expo SDK from 51 or earlier to 52+
- •TypeScript errors like:
- •
Property 'cacheDirectory' does not exist on type 'typeof import("expo-file-system")' - •
Module '"expo-file-system"' has no exported member 'EncodingType' - •
Property 'deleteAsync' does not exist - •
Property 'getInfoAsync' does not exist
- •
- •Runtime issues where
file.inforeturns a function instead of file metadata - •Any code importing
import * as FileSystem from 'expo-file-system'
Solution
1. Change Import Style
// OLD (legacy API)
import * as FileSystem from 'expo-file-system';
// NEW (class-based API)
import { File, Directory, Paths } from 'expo-file-system';
2. Replace Common Operations
| Legacy API | New Class-Based API |
|---|---|
FileSystem.cacheDirectory | Paths.cache |
FileSystem.documentDirectory | Paths.document |
FileSystem.deleteAsync(uri, { idempotent: true }) | new File(uri).delete() |
FileSystem.getInfoAsync(uri) | new File(uri).exists (for existence check) |
FileSystem.writeAsStringAsync(uri, content, { encoding: 'base64' }) | new File(path, name).write(content, { encoding: 'base64' }) |
FileSystem.readAsStringAsync(uri) | new File(uri).text() |
3. File Creation Pattern
// OLD
const filePath = `${FileSystem.cacheDirectory}voice_${Date.now()}.mp3`;
await FileSystem.writeAsStringAsync(filePath, base64Data, {
encoding: FileSystem.EncodingType.Base64
});
// NEW
const fileName = `voice_${Date.now()}.mp3`;
const file = new File(Paths.cache, fileName);
await file.write(base64Data, { encoding: 'base64' });
const uri = file.uri;
4. File Deletion Pattern
// OLD
await FileSystem.deleteAsync(uri, { idempotent: true });
// NEW
try {
const file = new File(uri);
if (file.exists) {
await file.delete();
}
} catch {
// Ignore delete errors (equivalent to idempotent)
}
5. File Info Pattern
// OLD
const info = await FileSystem.getInfoAsync(uri);
if (info.exists && info.modificationTime) {
// use info.modificationTime
}
// NEW - Note: info is now a METHOD, not a property
const file = new File(uri);
if (file.exists) {
// For modification time, use filename timestamps or other approach
// The new API structure differs for file metadata
}
6. Directory Listing
// OLD
const files = await FileSystem.readDirectoryAsync(cacheDir);
// NEW
const cacheDir = Paths.cache;
const contents = cacheDir.list(); // Returns array of File/Directory objects
for (const item of contents) {
if (item instanceof File) {
console.log(item.name, item.uri);
}
}
Verification
After migration:
- •Run
npx tsc --noEmit- should pass with no expo-file-system errors - •Test file operations in the app to ensure they work correctly
Example
Complete before/after for a voice message cache cleanup:
// BEFORE (Legacy API)
import * as FileSystem from 'expo-file-system';
export async function cleanupAudioCache(maxAgeMs: number): Promise<void> {
const cacheDir = FileSystem.cacheDirectory;
if (!cacheDir) return;
const files = await FileSystem.readDirectoryAsync(cacheDir);
const now = Date.now();
for (const fileName of files) {
if (fileName.startsWith('voice_') && fileName.endsWith('.mp3')) {
const filePath = `${cacheDir}${fileName}`;
const info = await FileSystem.getInfoAsync(filePath);
if (info.exists && info.modificationTime) {
const age = now - info.modificationTime * 1000;
if (age > maxAgeMs) {
await FileSystem.deleteAsync(filePath, { idempotent: true });
}
}
}
}
}
// AFTER (Class-Based API)
import { File, Paths } from 'expo-file-system';
export async function cleanupAudioCache(maxAgeMs: number): Promise<void> {
const cacheDir = Paths.cache;
const contents = cacheDir.list();
const now = Date.now();
for (const item of contents) {
if (item instanceof File &&
item.name.startsWith('voice_') &&
item.name.endsWith('.mp3')) {
// Extract timestamp from filename (voice_{timestamp}.mp3)
const match = item.name.match(/voice_(\d+)\.mp3/);
if (match) {
const timestamp = parseInt(match[1], 10);
const age = now - timestamp;
if (age > maxAgeMs) {
await item.delete();
}
}
}
}
}
Notes
- •The new API uses classes (File, Directory) rather than functions
- •
Pathsobject provides standard directories (cache, document, etc.) - •File metadata access has changed -
infois now a method, not a property - •The new API is more object-oriented and TypeScript-friendly
- •Some operations that were async are now sync (like
exists,list())