-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: Types as Configuration #58025
Conversation
Looks like you're introducing a change to the public API surface area. If this includes breaking changes, please document them on our wiki's API Breaking Changes page. Also, please make sure @DanielRosenwasser and @RyanCavanaugh are aware of the changes, just as a heads up. |
@typescript-bot pack this |
Hey @weswigham, I've packed this into an installable tgz. You can install it for testing by referencing it in your
and then running There is also a playground for this build and an npm module you can use via |
Is this also targeting 5.5.555 or is this evil (eval?) enough to land in 6.6.666? |
I can't even tell if this is an April Fool's joke or not 😭 |
Code as Configuration is something I have been wanting forever. I hope this was not a April Fool’s joke😭. |
Recently, we went over some ideas for new and interesting ways to merge config files. Ever since, or maybe, since forever, I've been thinking about the problem. The progression, give or take a step, for most command-line tools is this:
@filename
on the CLI).npmrc
,.vscode/settings.json
,eslint.json
,tsconfig.json
)webpack.js
,eslint.js
,Herebyfile.mjs
)We have the first three and have been reluctant to make the leap to the fourth. There are various reasons stated in different places, but mostly it comes down to not wanting or being able to execute untrusted code (especially in the editor). We're not in the business of sandboxing a dynamic, interpreted language, like javascript. Philosophically, there is little disagreement that a config file like this:
is fairly compelling - solving most of the issues we have with the static files of today (inflexible merging and imports, inability to create calculated fields generally) without needing to reinvent the wheel and build a custom procedural transform definition language in a static context.
But I couldn't help but wonder - sure, we have our reasons why we didn't like Code as Configuration for TypeScript - but what if we went beyond that - what if we evolved our configuration into something even greater - more expressive, yet structured than code itself. What if we used types as configuration. After all, why not use what we have available? For us, checking is basically as easy as JSON parsing. And our types are, somehow, a sandboxed, dynamic, interpreted language unto themselves. People do a lot of crazy things in our typesystem, and it's only gotten more expressive primitives over time. Imagine with me, for a moment. You could pretty easily define the shape of a tsconfig as a type, eg:
and then stick it in a known location
and that's that - just look the type pointed at by
default
, and transform it into a config object1.Now that... could work... but composability? Something like
maybe? That could work... it is flexible and expressive enough, but it is a bit wordy, especially compared to options today. Maybe some aliases would help. How's completion support inside the type? Missing? Well, that's a downgrade. Guaranteeing schema conformance would be nice. So we also need a helper like
so we can write
and that gives us safety... still no completions, though. Only way to get that would be.... in... an... inference... context...
And this is where you realize we've been going about this exercise all wrong - types are great - we let you write a type that can exactly describe the precise single value of a json object (mostly), but a lot of the greatness of our typesystem isn't in its' constructors, rather it is in how it maps to the host language - javascript. In fact, it's more powerful in the context of javascript expressions than what type constructors alone can provide. That's when you realize if you write a definition like
you can invoke it like so
and get full completions and typechecking on the call, and still get exact literal types for the type of the expression, provided all operations done within the expression resolve to exact literal types. Which means we've come full circle - the idealized form of types as configuration is actually
or... visibly identical to code as configuration. The only difference being that I can't believe we didn't use eval to get there2.
This PR is a working proof of concept - it will load a
tsconfig.d.ts
,tsconfig.ts
, ortsconfig.js
(or any other code extension), and look for the type of thedefault
member of that module3, convert the type to a config object, and use that config object. For example, the above:this should work at a basic level both on the command line and in the editor, but this shouldn't be regarded as anything close to production ready - it'd need tests, a dedicated language service project for type config files, more tests, actual diagnostics for when the
default
type is, say, circularly referential or non-literal, proper integration with caching in the language service layer, a less layered approach to the API (type -> json -> compiler options is indirect - type -> compiler options directly probably yields better UX), and tests.Still, this is potentially useful, even as incomplete as this - playing around with it has made it apparent, to me at least, just how much I didn't care about the file being executed, but really did just want the syntactic niceties like imports and spreads. And maybe a function call or two. Things not hard to represent purely at compile time in the type system we have today, but that are terribly painful in a JSON document.
Footnotes
Waves hand at the specifics of the transform in the cases of unions, intersections, optionals, generics, etc. - Only literal, object, and tuple types matter here - you probably wanna error on types that contain anything else. ↩
Though you could
eval
it and get the same thing... Unless you void your warranty by casting. ↩More handwaving about how that module is loaded. - Suffice to say, you just pick one set of compiler settings to load configs with (
nodenext
-y,moduleDetection: "force"
) and stick with it. ↩