file-type
file-type tells you what a binary file actually is by reading its magic number, the few signature bytes at the start (and sometimes deeper inside) that identify a format. You hand it a path, a Uint8Array, a Blob, or a web ReadableStream and get back {ext, mime}, or undefined when nothing matches. It covers a few hundred formats: images, audio, video, archives, fonts, Office Open XML, and more, and it reads only as much of the input as it needs, so checking a 4 GB video costs a few kilobytes. The point is to stop trusting the filename extension and the browser-supplied Content-Type, both of which the client controls.
The right default for answering "what is this file really" in Node, and the maintenance of the signature table is the whole value. Just remember it is a hint and not a gate: pair it with a size limit, an allowlist, and a timeout before you point it at anything a stranger uploaded.
Use it if
- You accept uploads and need to know what the bytes are, not what the client claimed: an attacker renaming shell.php to avatar.png defeats extension checks but not a signature check
- You are working with big or remote files: fileTypeFromBlob and fileTypeFromStream read only the head, and the tokenizer interface lets @tokenizer/http or @tokenizer/s3 pull a few ranged bytes instead of the whole object
- You want the format table maintained by someone else: keeping signatures for a few hundred formats correct, including the ZIP-container formats where docx, xlsx, and epub all start with PK, is not work you want to own
- You need to route by real type: pick a thumbnailer by mime, reject anything not in an allowlist, or fix a wrong extension before storing the file
- Your project is CommonJS: v22 is pure ESM, so require('file-type') throws, and your options are the load-esm helper, converting to ESM, or the frozen version-16 tag
- You are on Node 20 or earlier: v22 sets engines.node to >=22 and dropped Node stream.Readable support, so fileTypeFromStream now takes a web ReadableStream only and you have to wrap with Readable.toWeb()
- You expect it to cover text formats: .txt, .csv, and .svg are explicitly out of scope because they have no reliable signature, and Office 97-2003 files (.doc, .xls, .ppt) plus .msi need the separate @file-type/cfbf detector
- You are treating it as a security boundary: the README says detection is a best-effort hint and hardening against malformed input is best-effort too. March 2026 shipped fixes for a ZIP bomb (GHSA-j47w-4g3g-c36v) and an infinite loop in the ASF parser (GHSA-5v7r-6r5c-r473), so untrusted input still needs a size cap and a worker with a timeout
- You are shipping this to the browser for two or three formats: v22 removed the sub-exports like file-type/core, so you import the whole detector table plus four dependencies, and magic-bytes.js does a small allowlist for a fraction of the bytes
Setup reality
npm install file-type is the whole install, with TypeScript types included and four dependencies (strtok3, token-types, uint8array-extras, @tokenizer/inflate) that come along quietly. Everything else is module-format pain. The package is pure ESM and has been for years, so CommonJS projects hit ERR_REQUIRE_ESM and older Webpack setups need current versions and correct ESM configuration. v22 raised the Node floor to 22, removed all sub-exports, and made web ReadableStream the only accepted stream type, which breaks every fs.createReadStream call site until you wrap it in Readable.toWeb. There is no synchronous API: every entry point returns a Promise, including the buffer one, which surprises people writing validation middleware. And the result is undefined rather than a throw when nothing matches, so a missing check reads as a passed check.
Patterns
Detect the type of a file on diskdetect-from-file
import {fileTypeFromFile} from 'file-type';
console.log(await fileTypeFromFile('Unicorn.png'));
//=> {ext: 'png', mime: 'image/png'}Returns undefined, not an error, when nothing matches, so destructuring the result blows up on unknown files. Only works where node:fs exists; use fileTypeFromBlob for a browser File.
Detect from bytes you already havedetect-from-buffer
import {fileTypeFromBuffer} from 'file-type';
import {readChunk} from 'read-chunk';
const buffer = await readChunk('Unicorn.png', {length: 4100});
console.log(await fileTypeFromBuffer(buffer));
//=> {ext: 'png', mime: 'image/png'}4100 bytes is the sample size the library itself uses; shorter samples lower the hit rate because some formats put their signature well past the first bytes. Takes Uint8Array or ArrayBuffer, and it is async despite doing no IO.
Check a browser File before uploading itdetect-from-blob
import {fileTypeFromBlob} from 'file-type';
const file = input.files[0]; // a File is a Blob
const type = await fileTypeFromBlob(file);
if (type?.mime !== 'image/jpeg') {
throw new Error('Please pick a JPEG');
}It streams the Blob rather than loading it, so this is safe on large files. Client-side checks are a UX nicety only; the same check has to run on the server, where the bytes cannot be forged in DevTools.
Detect from a web ReadableStreamdetect-from-stream
import {fileTypeFromStream} from 'file-type';
const response = await fetch(url);
console.log(await fileTypeFromStream(response.body));
// Node stream? convert it first (required since v22):
import fs from 'node:fs';
import {Readable} from 'node:stream';
await fileTypeFromStream(Readable.toWeb(fs.createReadStream('file.mp4')));v22 dropped Node stream.Readable entirely. Passing one now fails instead of working, which is the single most common upgrade break from v21.
Detect without consuming the streamdetect-in-pipeline
import {fileTypeStream} from 'file-type';
const response = await fetch(url);
const stream = await fileTypeStream(response.body, {sampleSize: 1024});
if (stream.fileType?.mime === 'image/jpeg') {
// stream still replays from byte zero
}It buffers sampleSize bytes (default 4100) so downstream consumers still see the whole file. Shrinking sampleSize saves memory per concurrent upload at the cost of detection accuracy.
Allowlist upload types on the servervalidate-upload
import {fileTypeFromBuffer} from 'file-type';
const ALLOWED = new Set(['image/jpeg', 'image/png', 'image/webp']);
async function check(bytes) {
const type = await fileTypeFromBuffer(bytes);
if (!type || !ALLOWED.has(type.mime)) {
throw new Error('Unsupported file type');
}
return type.ext; // use this, not the client's filename
}Allowlist, never blocklist, and treat undefined as a rejection. Also cap the request body size and run this in a worker with a timeout: the project treats malformed-input hangs as bugs, not as security issues.
Ask what it can detectlist-supported-types
import {supportedExtensions, supportedMimeTypes} from 'file-type';
console.log(supportedMimeTypes.has('image/avif')); // true
console.log(supportedExtensions.size);Both are Sets, handy for validating a configured allowlist at startup so a typo in your config fails on boot instead of rejecting every upload silently.
Write a detector for your own formatcustom-detector
import {FileTypeParser} from 'file-type';
const unicornDetector = {
id: 'unicorn',
async detect(tokenizer) {
const header = [85, 78, 73, 67, 79, 82, 78]; // "UNICORN"
const buffer = new Uint8Array(header.length);
await tokenizer.peekBuffer(buffer, {length: header.length, mayBeLess: true});
return header.every((v, i) => v === buffer[i])
? {ext: 'unicorn', mime: 'application/unicorn'}
: undefined;
},
};
const parser = new FileTypeParser({customDetectors: [unicornDetector]});
console.log(await parser.fromBuffer(bytes));Use peekBuffer, not readBuffer. If your detector advances tokenizer.position and then returns undefined, the chain stops dead and the whole result is undefined, because later detectors can no longer see the start of the file.
Add support for XML and legacy Office formatsthird-party-detectors
import {fileTypeFromFile} from 'file-type';
import {detectXml} from '@file-type/xml';
const type = await fileTypeFromFile('sample.kml', {customDetectors: [detectXml]});This is how you get svg, kml, and rss, which the core package refuses on purpose. @file-type/cfbf covers .doc, .xls, .ppt, and .msi; @file-type/av sharpens audio versus video calls.
Cancel a slow detectionabort-detection
import {FileTypeParser} from 'file-type';
const controller = new AbortController();
const parser = new FileTypeParser({signal: controller.signal});
const promise = parser.fromStream(blob.stream());
setTimeout(() => controller.abort(), 2000);The signal goes on the FileTypeParser constructor, not on the standalone helper functions. Only async operations honor it, so a pathological in-memory buffer parse can still block the event loop; a worker thread is the real fix.
Identify a remote file without downloading itremote-range-reads
import {makeTokenizer} from '@tokenizer/http';
import {fileTypeFromTokenizer} from 'file-type';
const tokenizer = await makeTokenizer(audioTrackUrl);
console.log(await fileTypeFromTokenizer(tokenizer));
//=> {ext: 'mp3', mime: 'audio/mpeg'}A tokenizer can seek, so it fetches a handful of HTTP ranges instead of streaming the file. @tokenizer/s3 does the same against an S3 client, which is the cheap way to audit a bucket.
Detect sloppily muxed MP3 and AAC filesmpeg-tolerance
const type = await fileTypeFromFile('weird.mp3', {mpegOffsetTolerance: 10});Defaults to 0, meaning the first MPEG audio frame must sit exactly where the parser expects. A tolerance of 10 bytes covers most real-world files that are technically invalid but play fine everywhere.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| magic-bytes.js | npm | You want a small synchronous signature check in the browser, or you only care about a handful of formats and cannot afford the full table. |
| mime-types | npm | The filename is genuinely trustworthy (your own generated files) and you just need extension to MIME mapping without reading any bytes. |
| mmmagic | npm | You want libmagic's coverage, including text formats and encodings, and can accept a native addon in your build and deploy. |