Since you did ask about best practices, it's not advised to modify the prototype of the type that's not in your control or use custom variables on top of it. While JavaScript is rather loose and lets you make these changes, you should guard yourself against any further changes to the prototype that Socket.IO may make in the future.
The reason is that there's always a chance, however small, of there being a collision with whatever property name you're using.
To get around this, we can either use a Map related to the socket ID, or use a WeakMap on the socket itself. If you use a Map, you have to remove the entry manually when the socket is disconnected, but with a WeakMap, it's garbage collected automatically (assuming SocketIO releases all references to it on disconnect).
/** @type {WeakMap<SocketIO.Socket, Object>} */
const socketIOLocals = new WeakMap();
io.use((socket, next) => {
const locals = { player: null }; // Create new instance
socketIOLocals.set(socket, locals);
next();
});
You can then get the variable object with socketIOLocals.get(socket);
On a side note, Express does have the .locals property to let you pass data between Response objects. It's a shame SocketIO doesn't have something like this.
socket.player = whatever.