Hey i saw your message on the issue.
So here is a quick solution (and an explanation)
First: addCheck
doesnt work for submodules at all. This is not widely known in nixpkgs.
All places that you found there are most likely not working and the maintainers of that code are most likely not aware (yet). Hopefully they will get aware, or we manage to fix addCheck, such that they don’t have to bother at all.
I am not sure if this kind of checking would even work in a lazy way for submodules, since we perform checking before merging. Meaning use cases where range.start
is in one module and range.end
in another couldn’t be merged because we need to check first. Check is only very shallowly checking the surface.
Solutions:
Method 1:
To ‘check’ a submodule for integrity using the type’s check
is not working and may break laziness. To circumvent that problem you can add a message to assertions
or warnings
(in nixos).
Like this:
({config, ...}: {
assertions = [
{
assertion = config.range.start <= config.range.end;
message = "please specify a valid range";
}
];
})
In nixos assertions
and warnings
are evaluated as part of certain output attributes such as system.build.toplevel
nix-repl> nixos.config.system.build.toplevel
# ... elided
Failed assertions:
- please specify a valid range
If not on nixos you could build your own seq
or deepSeq
and mimic the strategy of nixos in your own module system.
Method 2:
Create a custom type:
A bit more complex, see examples how to create types in nixpkgs 
Benefit: Works with all modules, not just nixos.
Some more insight:
The reason the submodule type check is discarded is this little function in the modules.nix
file:
Needs to be fixed, thought this requires some creative idea that i didnt manage to find yet or a complete refactor of the types. 
fixupOptionType =
loc: opt:
if opt.type.getSubModules or null == null then
opt // { type = opt.type or types.unspecified; }
else
lib.trace "DISCARD HAPPENED :)" (
opt
// {
type = opt.type.substSubModules opt.options; # <- This line here re-constructs the submodule type with its native check. That means any added check is always discarded siltently :/
options = [ ];
});
nix-repl> eval.options.range
trace: DISCARD HAPPENED :)
{
__toString = «lambda __toString @ lib/modules.nix:1095:20»;
_type = "option";
declarationPositions = [ ... ];
declarations = [ ... ];
default = { ... };
definitions = [ ... ];
definitionsWithLocations = [ ... ];
files = [ ... ];
highestPrio = 1500;
isDefined = true;
loc = [ ... ];
options = [ ... ];
type = { ... };
value = { ... };
}