The important part is that the interface has to have the same semantics as a config file: atomic/transactional. For example the builder pattern does this. You can enjoy all the expressiveness and power of the high-level language, but at some point you create the final config/pipeline/job-graph, call whatever needs to be called, and the provider of the interface needs to validate that.
That's why the semantics of Terraform is better than Ansible's.
I'd much rather use python/jinja/gomplate/what-have-you, do all my fancy expressive stuff with code I can check in, and get a single snapshot of plain, structured text (which I can also check in). Not to mention locking down your invariants (I can't recall how many times I've gotten bit by interpolating docker image uris with the wrong TAG). It's actually pushed me towards first Make and then later python cli tools (cause Makefile syntax is...ugh) to generate configs, rather than relying on any runtime business logic, cause it's just so much saner, traceable, auditable, etc.
Sure, it is still Bash, it still requires selling your soul, but it's not like I was going to Haskell heaven anyway.
Further, these configs usually act as declarative languages, giving directives to pipelines, controllers, etc., typically with idempotent side-effects.
This is just good software engineering. You have some static data structure which describes some result you want to exist in the world, and your pipeline/program makes that outcome manifest. This is the essence of boundary programming/hexagonal architecture.
https://www.destroyallsoftware.com/talks/boundaries