mrkeyoor.com_
Thu 06 Aug 02:42 UTC
npmUtilsupdated 05 Aug 2026

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.

Verdict

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.

API stability3/5Function names have been stable since the v16 rewrite, but majors land often and they break: v22 alone raised Node to 22, deleted every sub-export, dropped Node Readable streams, and renamed a stream type. Pinning and reading release notes is not optional here.
Docs5/5The README documents every entry point with a runnable example, lists all supported extensions with links, names the formats it refuses and why, explains the custom detector protocol including what happens when a detector moves the tokenizer, and states the security limits plainly.
Maintenance5/5Pushed April 2026 with zero open issues and PRs, maintained by Sindre Sorhus and Borewit, and the March 2026 advisories were fixed and released within days of report.
Ecosystem4/5The default file sniffer in the Node world, with an official CLI and a set of first-party-adjacent detector plugins (@file-type/xml, @file-type/cfbf, @file-type/av, @file-type/pdf) plus tokenizer adapters for HTTP and S3. The ESM-only stance keeps it out of a large slice of older codebases.

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
Skip it if

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

PackageRegistryPick it when
magic-bytes.jsnpmYou 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-typesnpmThe filename is genuinely trustworthy (your own generated files) and you just need extension to MIME mapping without reading any bytes.
mmmagicnpmYou want libmagic's coverage, including text formats and encodings, and can accept a native addon in your build and deploy.