Open-source project
vercel/ms avatar
vercel/ms

ms, a 4.0.0 tree whose newest published release is a canary from 2021

Tiny millisecond conversion utility

5,563 stars348 forksTypeScriptMIT

At a glance

What is it?
The tiny millisecond converter is now ESM only, built with tsdown, tested twice against two runtimes, and linted by Biome. What the page does not reconcile is that the manifest sits at version 4.0.0 while the newest GitHub release is a canary tagged in September 2021.
Who is it for?
This fits new code that ships ESM and already depends on Node 20 or newer, where a small converter tested under both Node and the edge runtime is easier to trust than a hand rolled one. Check four things first.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 178 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 14, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • vercel/ms5,563 ★ · +9
Star history
Repository2026-09-242026-11-14
vercel/ms5.6k5.6k

9 Stars in the last 51 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

The manifest says 4.0.0 and the newest release is a canary from 2021

The root `package.json` declares version `4.0.0`:

json
{
  "name": "ms",
  "version": "4.0.0",
  "description": "Tiny millisecond conversion utility",

The releases available are `3.0.0-canary.1` and `3.0.0-canary.0`, both published on 2021-09-15 about two hours apart, and `2.1.3` from 2020-12-08. So the newest thing you can install from a tag is a canary build from five years ago, while the working tree is two majors ahead of the last stable. The README compounds it by saying that as of v3.0 the package includes TypeScript definitions, which is now two versions back rather than the current one. The last commit to `main` is dated 2026-05-20, so the code is still moving; it is the release trail that stopped, and nothing on the page explains the gap.

The exports map has no require condition, and a script enforces that

The package is `"type": "module"` and its exports map offers an `import` condition and a bare `default`, both pointing at `./dist/index.js`, with types at `./dist/index.d.ts`. There is no `require` entry anywhere, so a CommonJS caller has no supported way in. `"sideEffects": false` is set, and the published `files` array is a single entry, `dist`, so the tarball carries the bundled output and its declarations and nothing else. The build script is `tsdown`, and there is a dedicated check for the packaging shape: `attw` runs the build and then `@arethetypeswrong/cli` with `--pack --profile esm-only`, which fails if anything about the published types or module format does not match that profile. In other words the ESM-only constraint is not a convention here, it is a gate in the script list.

ms('1y') is 365.25 days, and no example formats a value back to a year

The parsing examples include one value worth doing arithmetic on:

ts
ms('2 days')  // 172800000
ms('1d')      // 86400000
ms('10h')     // 36000000
ms('2.5 hrs') // 9000000
ms('2h')      // 7200000
ms('1m')      // 60000
ms('5s')      // 5000
ms('1y')      // 31557600000

Dividing that last figure by the milliseconds in a day gives 365.25, not 365, so the year unit is a quarter of a day longer than a calendar year and the value is not a round number of days. The formatting examples only ever come back in minutes and hours, `ms(60000)` giving `"1m"` and `ms(ms('10 hours'))` giving `"10h"`, so the page never shows what a year long value formats back to, and months and weeks appear in the unit type without a single worked example of either. Negative input is covered for days, hours and bare numbers, so the sign handling is at least deliberate. Fractional values are called out separately: `0.5m`, `-0.5m`, `.5m`, and `-.5m` are all said to be supported, and a value with no unit at all defaults to milliseconds. The reverse direction is where the examples get thin, since the largest unit ever shown coming out of the formatter is hours.

The parseStrict example passes a variable that is not in scope

The strict parser's own snippet has a typo in it:

ts
import { parseStrict } from 'ms';

parseStrict('1h'); // 3600000

function example(s: string) {
  return parseStrict(str); // tsc error
}

The function is declared with a parameter named `s` and then passes `str`, which is never introduced, so the example cannot compile as printed. The comment marking it as a tsc error is about the wrong identifier: the intended failure is that a plain `string` is not accepted where a strict value is required, not that `str` is undefined. That matters because this is the one snippet that shows the point of `parseStrict`, which is that strict type checking for the input value rejects the loose case. The looser variants elsewhere on the page are correct, including the deliberate footgun that casts a `string` to `StringValue` and warns that it can be dangerous.

The unit list is lowercase only while the runtime accepts three casings

The printed input types are the lowercase set:

ts
type Years = 'years' | 'year' | 'yrs' | 'yr' | 'y'
type Months = 'months' | 'month' | 'mo'
type Weeks = 'weeks' | 'week' | 'w'
type Days = 'days' | 'day' | 'd'
type Hours = 'hours' | 'hour' | 'hrs' | 'hr' | 'h'
type Minutes = 'minutes' | 'minute' | 'mins' | 'min' | 'm'
type Seconds = 'seconds' | 'second' | 'secs' | 'sec' | 's'
type Milliseconds = 'milliseconds' | 'millisecond' | 'msecs' | 'msec' | 'ms'

The prose around it says the formats can be lowercase, uppercase, or capitalized, and that there can be a space or not, so `"2minutes"` and `"2 MINUTES"` both work at runtime. None of that is in the union above, which is why the page leans on template literal types instead: `StringValue` is exported for callers who want the narrow type, and the note says the casing and spacing rules are enforced at the type level when you use `parseStrict`. A reader who copies the `Unit` union as printed and types an uppercase string against it will be fighting their own compiler.

The bug report flow runs npm in a pnpm repository

The contributing section is a fork, clone, link, and test sequence:

bash
npm link
npm link ms

with `npm test` at the end. None of that matches the repository it is describing. The manifest pins `packageManager` to pnpm 10.33.0 with a full integrity hash, the tree carries `pnpm-lock.yaml` and `pnpm-workspace.yaml` for a single package, and the `test` and `attw` scripts call pnpm, with `test` being `pnpm run test:nodejs && pnpm run test:edge`. There is also an `.npmrc` that the page never mentions. Hooks are wired through husky, with `prepare` running husky, a `precommit` script running lint-staged, and a `.husky/` directory. Linting and formatting both point at Biome rather than at eslint or prettier, so `biome lint` and `biome format` are the two commands a contributor is expected to run. Type checking is a third separate command, `tsc --noEmit`, and the tree carries four config files to match: `jest.config.ts`, `tsconfig.json`, `tsdown.config.ts`, and `lint-staged.config.ts`.

Two test environments, and the edge one runs without coverage

The test script runs the suite twice:

json
"test": "pnpm run test:nodejs && pnpm run test:edge",
"test:nodejs": "jest --env node",
"test:edge": "jest --env @edge-runtime/jest-environment --no-coverage",

The edge environment is a separate package, `@edge-runtime/jest-environment` at 4.0.0, and its run passes `--no-coverage`, so the second pass contributes pass or fail and nothing to the coverage number. The README's edge example is the pages router form, a `pages/api/edge.js` file with `runtime: 'experimental-edge'`, installed with `next@canary` or a global `vercel@canary`, while the manifest requires Node 20 or newer, so the example predates the current App Router layout. Two loose ends sit at the end of the page: a Related Packages entry pointing at `ms.macro` for running `ms` as a build time macro, and a footnote definition for Internet Explorer that has no reference marker anywhere in the text above it.

Editorial conclusion

This fits new code that ships ESM and already depends on Node 20 or newer, where a small converter tested under both Node and the edge runtime is easier to trust than a hand rolled one. Check four things first. There is no stable 4.x release to install from a tag, so pin whatever you actually resolve. The package is ESM only, with no require condition in its exports map, so CommonJS consumers cannot require it. `ms('1y')` is 365.25 days rather than 365, which matters if you use it for anything calendar shaped. And use `parseStrict` where the input comes from outside your code, since the loose parser accepts any string the type claims to narrow.

Frequently asked questions

How do I install the ms package?

With a package manager, and the page lists all five forms: `npm install ms`, `yarn add ms`, `pnpm add ms`, `deno add npm:ms`, and `bun add ms`. It is described as working in browsers and in server runtimes including Node.js, Deno and Bun.

Can I use ms with CommonJS require?

Not through the published entry point. The package is `"type": "module"` and its exports map offers only an `import` condition and a bare `default`, both resolving to `./dist/index.js`, with no `require` entry. The manifest enforces that shape with an `attw` script that runs `@arethetypeswrong/cli` against the packed tarball with the `esm-only` profile.

What does ms do when I pass it a plain number?

It returns a string with a unit: `ms(60000)` gives `"1m"`, `ms(2 * 60000)` gives `"2m"`, and a negative value gives `"-3m"`. Passing a string containing a number returns a number instead, so `ms('100')` is `100`, and a bare number with no unit is treated as milliseconds.

Why does ms export parse, format and parseStrict?

Since v3.0 the helpers can be imported separately, with `parse('1h')` giving `3600000` and `format(2000)` giving `"2s"`. A long form output is available through the option object, so `ms(60000, { long: true })` gives `"1 minute"`. `parseStrict` exists for callers who want the input type checked rather than widened.

Which version of ms should I depend on?

The working tree is at version 4.0.0, while the newest visible releases are `3.0.0-canary.1` and `3.0.0-canary.0` from 2021-09-15 and `2.1.3` from 2020-12-08. The last commit to the default branch is dated 2026-05-20, so pin whatever your resolver actually gives you rather than assuming a tag.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. vercel/ms on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/vercel-ms.svg)](https://hysenlabs.com/projects/vercel-ms)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/vercel-ms.svg)](https://hysenlabs.com/projects/vercel-ms)

Was this useful?