~1.2.3 matches >=1.2.3 <1.3.0: 1.2.3 and every later 1.2.x patch release, and nothing from 1.3.0 up. The tilde keeps the major and minor numbers you wrote and lets only the patch rise.
Against real versions
1.2.2 no below the floor
1.2.3 yes
1.2.4 yes
1.2.99 yes the highest match
1.2.5-rc.1 no a prerelease
1.3.0 no a minor bump
2.0.0 no
How much you write matters
The tilde’s reach depends on how many numbers follow it, and below 1.0.0 it can be looser than the caret:
~1.2.3 >=1.2.3 <1.3.0
~1.2 >=1.2.0 <1.3.0 same as 1.2.x
~1 >=1.0.0 <2.0.0 same as 1.x and ^1
~0.2.3 >=0.2.3 <0.3.0 same as ^0.2.3
~0.0.3 >=0.0.3 <0.1.0 ^0.0.3 stops at 0.0.4
When to use it
A tilde suits a dependency whose minor releases have changed behavior you rely on, when you still want its bug fixes. The cost is that you don’t get fixes that ship only in 1.3 or later, and npm update won’t cross to 1.3.0 for you.
To have npm write tildes, pass --save-prefix='~' to one install, or run npm config set save-prefix='~' for every later one. In Yarn 2 and later, yarn add -T pkg writes a tilde; pnpm takes pnpm config set save-prefix='~'. A lockfile still pins the exact version: the tilde only bounds what npm update, or a fresh resolve, may choose.
npx semver -r '~1.2.3' 1.2.9 1.3.0 # prints 1.2.9
Composer and Cargo
Composer agrees on ~1.2.3, which is >=1.2.3 <1.3.0 there too, but reads ~1.2 as >=1.2 <2.0.0: in Composer the last number written is the one that may rise. In npm, ~1.2 stops at 1.3.0, so a constraint copied from composer.json to package.json can quietly narrow. Cargo matches npm: ~1.2.3 is >=1.2.3, <1.3.0, ~1.2 is >=1.2.0, <1.3.0 and ~1 is >=1.0.0, <2.0.0.