At runtime there wouldn't be a difference, but at compile time you can use a trick called branding to make the compiler think that UserId extends number:
type UserId = number & { __userId: true };
By intersecting number with an object type, we have defined a type which extends number but is not extended by number. Note that
const someUser = new User(1 as UserId);
is using a type assertion to lie to the compiler; 1 is not really a UserId according to that definition. And such lying is necessary because you don't really want to try to add a __userId property to a primitive number at runtime (you could mess with the Number prototype, but this would add such a property to every number, which defeats the purpose).
But it serves your needs:
usersIds.push(someUser.id); // okay
usersIds.push(2); // error!
// Argument of type 'number' is not assignable to parameter of type 'UserId'
Playground link to code