Chef isn't an alternative to NixOS. Chef is imperative and stateful as it could possibly get. The whole thing is piles upon piles of unrestricted Ruby code that gets evaluated
during deployment. NixOS has the exact opposite design.
Compared to NixOS, Chef's design allows for flexible deploy time decisions, but it comes at a price. With Chef, it's hard to ever predict everything that's going happen during deployment. Chef code takes a bunch of external inputs in the form of Chef attributes and also has full visibility into server state. All of this happens during deployment. So to make sense of what would happen during deployment, you'd have to run your Chef code in the exact same environment as your target server. To make things even more challenging, Chef code can easily mutate server state irreversibly unless you go to extreme lengths to prevent it.
NixOS, on the other hand, prioritizes predictability and reproducibility. It builds the files required for deployment beforehand in a sandboxed environment before the deployment phase, with all the necessary inputs fed upfront. During deployment, it mostly copies those files into an isolated location under /nix/store and creates symlinks to them in /etc and elsewhere to activate the system. Figuring out what would happen during deployment beforehand is a matter of inspecting the built files.
Furthermore, unlike Chef configuration, NixOS configuration doesn't concern itself with deploy time actions. Its focus is on expressing static configuration. The Nix language is used for this purpose, so comparing it the against unrestricted general purpose programming languages is a mistake. A better comparison would be languages like JSON, YAML, Jsonnet, and Dhall.