Open hediet opened 7 years ago
I could have sworn there was a repo full of awesome TS work like this, could anyone help me find it again? I know it had things like a solution to a Tower of Hanoi problem, prime number detection, subtraction, addition, etc
I wrote a text adventure in the typescript type system:
I could have sworn there was a repo full of awesome TS work like this, could anyone help me find it again? I know it had things like a solution to a Tower of Hanoi problem, prime number detection, subtraction, addition, etc
@crutchcorn Is this what you were looking for? https://github.com/ronami/meta-typing
@nimajneb YES! Thank you!
This is fascinating, but why is it an open issue? Is Turing completeness somehow a problem?
Might be time to enable that fancy new Discussions tab where things like this can take place. 😉
@mindplay-dk By being Turing Complete, the type system is inconsistent and has situations where contradictions occur and/or certain types can not be resolved. A related post might be informative.
why is it an open issue
given there are currently ~5000 open issues, one more doesn't hurt 😉
A related post might be informative.
It's worth noting that due to the instantiation limit we do know that TypeScript's Type System will always halt.
// checker.ts
if (instantiationDepth === 50 || instantiationCount >= 5000000) {
// We have reached 50 recursive type instantiations and there is a very high likelyhood we're dealing
// with a combination of infinite generic types that perpetually generate new type identities. We stop
// the recursion here by yielding the error type.
tracing?.instant(tracing.Phase.CheckTypes, "instantiateType_DepthLimit", { typeId: type.id, instantiationDepth, instantiationCount });
error(currentNode, Diagnostics.Type_instantiation_is_excessively_deep_and_possibly_infinite);
return errorType;
}
totalInstantiationCount++;
instantiationCount++;
instantiationDepth++;
👾 Sokoban Game in Pure TypeScript Type System
Just a pedantic tip, we might need to implement a minimal language to prove TypeScript is turing complete.
http://stackoverflow.com/questions/449014/what-are-practical-guidelines-for-evaluating-a-languages-turing-completeness https://sdleffler.github.io/RustTypeSystemTuringComplete/
How 'bout Brainf*ck https://github.com/sno2/bf
(I made it)
hmmm, it seems this cannot prove turing completeness. Nat in this example will always terminate. Because we cannot generate arbitrary natural number. If we do encode some integers, isPrime will always terminate. But Turing machine can loop forever.
The Brainf*ck interpreter can also have an infinite loop by the following program so it seems that your final note is over now:
// Type instantiation is excessively deep and possibly infinite.
type Output = BF<{
program: "+[]";
outputType: "ascii";
}>;
@sno2 that is an absolute delight, thank you
Turing completeness meaning all can describe computable problems can be solved.
Since 4.8 you can now make complex calculus : You can check complete implementation here : https://github.com/ecyrbe/ts-calc
@ecyrbe what does the n
mean in those numbers? I’ve never seen that.
It's the JavaScript Bigint notation for numbers that are too Big to be represented by a number: https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Global_Objects/BigInt
A little over a year ago, I decided I liked TypeScript.
In fact, I decided liked it so much, I didn't just want it to be stuck in my editor— I wanted to use the same syntax to validate my data at runtime! And what better way to achieve that than to use TypeScript's own type system to parse its own syntax.
Much to my surprise, what I ended up with was not a toy, but a remarkably performant static parser that allows definitions built from arbitrarily parenthesized and nested TS operators like |
, &
, []
etc. to be syntactically and semantically validated as you type, inferred by TypeScript, and reused by JS to validate runtime data.
At this point, it can handle hundreds of cyclic types seamlessly and import between scopes. As far as I'm aware, ArkType is the most advanced application of TypeScript's type system to date:
This is what it looks like to interact with a scope of 100 cyclic types:
Props to the TypeScript team- it's genuinely incredible that TS is so well-built that it can literally parse itself in real-time without being remotely optimized for it.
Considering this I think TS might as well implement a way for users to run arbitrary procedural code to destruct and construct types at compile time. There are numerous situations where you end up writing some type mapping magic that's doing O(n^2) or worse computation because you have to do something in a roundabout way when straightforward procedural code could do it in O(n).
okay this thread is crazy.
Considering this I think TS might as well implement a way for users to run arbitrary procedural code to destruct and construct types at compile time. There are numerous situations where you end up writing some type mapping magic that's doing O(n^2) or worse computation because you have to do something in a roundabout way when straightforward procedural code could do it in O(n).
not to mention the fact that only Functional Programming Galaxy Brains™️ can figure out how to do much more with the type system than generics and maybe mapped types.
the type system is effectively a functional programming language, weaved into a multi-paradigm language, with a completely different syntax - it resembles C# on the surface, and it obviously resembles JS below the surface, but it's extremely foreign (even hostile at times) to people coming from either of those languages.
I love TS despite this glaring design problem, and front-end development at any real scale would be unbearable without it - but unless at some point there's a reckoning, I'm starting to think tides will eventually turn and TS won't last. 😥
I wish they would discuss possible avenues such as #41577 to break free of this.
A little over a year ago, I decided I liked TypeScript.
In fact, I decided liked it so much, I didn't just want it to be stuck in my editor— I wanted to use the same syntax to validate my data at runtime! And what better way to achieve that than to use TypeScript's own type system to parse its own syntax.
Much to my surprise, what I ended up with was not a toy, but a remarkably performant static parser that allows definitions built from arbitrarily parenthesized and nested TS operators like
|
,&
,[]
etc. to be syntactically and semantically validated as you type, inferred by TypeScript, and reused by JS to validate runtime data.At this point, it can handle hundreds of cyclic types seamlessly and import between scopes. As far as I'm aware, ArkType is the most advanced application of TypeScript's type system to date:
arktype.mp4
This is what it looks like to interact with a scope of 100 cyclic types:
typePerf.mp4
Props to the TypeScript team- it's genuinely incredible that TS is so well-built that it can literally parse itself in real-time without being remotely optimized for it.
Deepkit Runtime Types is far more advanced and you only have to use TypeScript types.
Deepkit Runtime Types support complex types such as the following: https://github.com/deepkit/deepkit-framework/commit/934192c85a4afc435b521bee6d0329b1bc42a805
@marcus-sa Deepkit is a very cool project, but it is not built within TypeScript's type system.
Rather it parses native TS types externally and adds runtime metadata, so it's orthogonal to this thread.
Two years ago I wrote a toy language interpreter using typescript's type system in order to learn typescript's type gymnastics. Here's the repository, here's the playground, and after that I didn't go any further...
I've been curious about writing types to parse GraphQL queries at type time, and, together with generated types from the schema, determine the query data and variables types. I've been using codegen for this for a long time but it gets kind of annoying.
I found a medium article where one dev tried this and ran into a lot of recursion depth limits.
This is one example of a case where it would be better if we could just use loops, stacks, and other procedural constructs at type time, rather than trying to use declarative constructs to accomplish the same thing.
Some people might say running procedural code at type time is a scary pandora's box kind of situation.
But pandora's box is already open! The reality of complicated types slowing down the language server or hitting the recursion depth limit is a lot more of a nightmare than the idea of running efficient procedural code, if you think about it.
We're really of the opinion that code to generate types from a source artifact should run once (when the source artifact changes), not once per keystroke (what happens if you do it in the type system via whatever means).
@jedwards1211 I actually agree both with you in that there's room for better abstractions here and @RyanCavanaugh that they shouldn't be a part of the language itself.
I mentioned ArkType earlier in the thread if you're interested in a highly-optimized type-level string parsing strategy.
Currently, the runtime and type-level implementation parsers are implemented in parallel by hand:
We've tossed around the idea a bit of creating a package that would provide an API for defining a parser that could generate optimized types and a runtime implementation from a single definition.
Probably not our top-priority, but if you're interested in contributing, I'd love to help facilitate making those strategies more reusable! Feel free to reach out on our community Discord.
@RyanCavanaugh
not once per keystroke
I don't understand, doesn't the language server already re-evaluate as you type?
@ssalbdivad even though ArkType is highly optimized, the size of strings it can parse must be limited by TS recursion depth, right? Since ArkType lets you mix object literals and type strings, a lot of the example strings it parses are short. GraphQL queries tend to be much larger.
@jedwards1211 Yes, but you could make a very practical gql parser within those limitations.
Character limit is 1001:
Expressions can have up to 46 operands:
And that is with arbitrary grouping, syntactic semantic validation (type-level error messages) etc.:
If it's possible to represent a definition while leveraging JS's built-in structures (object and tuple literals), yes there is a performance advantage to doing so, in addition to other benefits like better highlighting and formatting.
That said, with the right approach the type system will take you quite far, even with pure strings.
@ssalbdivad IMO a 1001 character limit isn't acceptable for this GraphQL use case in a production project, in general no one would want a compiler to have such a severe limit. I will just continue to wish that more were possible.
@jedwards1211 What you have to consider is the boundary between a "type" (ideally something that can be compared to other types and tested for assignability) and arbitrary linting/static analysis.
What you're describing sounds much more like the second category, so maybe an extension to facilitate the DX you want would be ideal. I do understand the appeal of being able to ship something like that through native TS but it's pretty firmly outside the realm of "types" at that point.
@ssalbdivad I'm not sure what kind of linting you think I mean, I really am just talking about computing the input and output TS types for a GraphQL query. And a type is a type, whether it's created by codegen-ing TS or by using TS type magic. If TS type magic is able to do it for short strings, it feels unfortunate if the length limit is some arbitrary, low number.
@jedwards1211 What I mean is that the arguments you pass to a generic type are themselves types. They might happen to mirror a literal value, but that will not always be the case.
When you talk about "loops, stacks, and other procedural constructs at type time", I assume you mean writing something like arbitrary imperative logic, which operates on values or terms, not types.
If you want them to be limited to a similar set of operations as can be expressed today via conditionals and mapped types, then sure, loops might be a mild syntactic convenience, but what you really want is perf optimizations and an increased recursion depth limit.
TS is already extremely performant in terms of how it handles this kind of parsing, so the increased limit is probably the more reasonable request of the two.
That said, I'd be surprised if there's not a way to split up a well-structured query such that it could fit within those limits. Maybe I'm misunderstanding and I'm definitely rusty when it comes to GraphQL, but to me, 1000 characters for a single query sounds like a nightmare.
@ssalbdivad no I'm talking about a bold idea (probably too bold, but I always want more powerful capabilities) for a syntax/API for declaring functions that are called with/return API representations of TS types, kind of like the API for inspecting types from the compiler that you can access in ts-morph
.
For example:
// new syntax, like a type alias but the code inside runs at type time
type function GreaterThan(A extends number, B extends number) {
const aValue = A instanceof TS.NumberLiteral
? A.value
: A instanceof TS.Union && A.options.every(opt => opt instanceof TS.NumberLiteral)
? Math.min(...A.options.map(opt => opt.value)
: undefined
const bValue = ... // likewise, but get max value of union
return aValue == null || bValue == null ? TS.factory.never() : aValue > bValue ? TS.factory.true() : TS.factory.false()
}
// and voila, now it works for number literals of any magnitude:
type Test = GreaterThan<72374812, 8600200> // true
And then for something like parsing ArkType, if the function received a string literal type, it would parse it into an AST using procedural code that isn't limited by the size of the stack, and convert the AST into these TS type representations.
One thing about this TS maintainers would probably not like is, it would be hard to guarantee these type functions are pure and deterministic. With great power comes great responsibility I guess... In any case it would probably be unworkable for reasons like, how do you compute variance?
But I hope to at least make the case that having a Turing-complete system limited to mere thousands of items is really annoying. It's like Genie said in Aladdin..."phenomenal cosmic powers...itty bitty living space."
I wish TS were at least capable of instantiating tail-recursive types without nested invocations, so that we'd no longer be bound by these limits, and type decls would work more like actual functional programming languages.
And no, 1000 characters in a single GraphQL query isn't uncommon or a nightmare by any means. There are three such queries in the production app I'm working on; here is one example.
Maybe this belongs in #41577 instead? (and #39385 is relevant)
not once per keystroke I don't understand, doesn't the language server already re-evaluate as you type?
If you have something like
// In file stuff.ts
interface Stuff {
foo: string;
bar: string;
}
// In the edited file
const p: Stuff = { foo: "hello", <- user is in the process of typing this
At each keystroke, the type of Stuff
is getting recomputed, but this work is extremely minimal since it's all statically defined. The AST isn't changing, the symbol maps aren't changing, it's very very cheap.
If instead you had
// In file stuff.ts
type Stuff = ParseMyCoolDSL<"[[foo], string], [[bar], string]]">
// In the edited file
const p: Stuff = { foo: "hello", <- user is in the process of typing this
Similarly here, ParseMyCoolDSL
is being re-evaluated every time, but this is a lot more work, and it's work (we as humans who know what the dependency tree is) that always results in the exact same shape. All the work of breaking apart the string, creating the temporary types, etc, creates work for the GC, and is just more work.
Even if we moved ParseMyCoolDSL
into some "Just write code to make types" (JWCTMT) system then we have basically the same problem; the same work is being redone over and over and over again to produce the exact same result.
If we ever come up with a way to re-use type computations between program versions (still a great goal and still very much plausible IMO), then the non-code-based version of ParseMyCoolDSL
is maybe something that could be identified to be "pure" (e.g. not dependent on inputs present in the file being edited), but the JWCTMT system is not going to work for that because we wouldn't be able to plausibly identify what inputs arbitrary code is depending on. JWCTMT sacrifices all possible future gain for some convenience today and I think it's the wrong thing to be asking for.
What @RyanCavanaugh describes is also a huge part of the reason that reusing native structures when representing shapes makes a lot of sense- incremental results, syntax highlighting, formatting.
Personally, I'd much rather read a giant gql query like the one you linked with each layer of the query defined individually rather than having everything in-lined. It's more reusable, and allows you to name various key sets based on how they're designed to be used which makes the intent of queries like that clearer compared to a single root name.
Again, definite caveat that I have little familiarity with "best practices" for these scenarios in GraphQL, but those concepts in engineering seem to be pretty universal.
If any change has the potential to create a great API for something like this, IMO it would be this one:
https://github.com/microsoft/TypeScript/pull/49552
Being able to compose your queries together naturally like that could offer an amazing DX while allowing you to break up your definitions into clear, composable units.
@ssalbdivad happy to take this discussion elsewhere if you have an idea where, but even if I broke the query text up into interpolated variables (definitely a good suggestion), magic TS types would still have to parse just as many characters, right?
Edit: I guess you're saying one type could parse a chunk that gets interpolated, and another type could parse the surrounding text. It would probably be too verbose for me to completely love it, but it would definitely help us push past the current limits!
@ssalbdivad IMO a 1001 character limit isn't acceptable for this GraphQL use case in a production project, in general no one would want a compiler to have such a severe limit. I will just continue to wish that more were possible.
There are posibilities, I had done some...things long time ago, but I abandoned it shamefully...That's why I never shared it here because of this embrassing fact. Here's the magic.
Take a try at parsing some long javascript-like code with it, it should be compiled into a weired opcode (I did intend to write a vm for that...). Basically it takes advantage of LR parsing, and use some tricks to break through the limits.
Luckily, a friend had translated my thoughts into English, hope it can be helpful. BTW, I'm not looking for a job anymore, I found a nice one :p
This is not really a bug report and I certainly don't want TypeScripts type system being restricted due to this issue. However, I noticed that the type system in its current form (version 2.2) is turing complete.
Turing completeness is being achieved by combining mapped types, recursive type definitions, accessing member types through index types and the fact that one can create types of arbitrary size. In particular, the following device enables turing completeness:
with
TrueExpr
,FalseExpr
andTest
being suitable types.Even though I didn't formally prove (edit: in the meantime, I did - see below) that the mentioned device makes TypeScript turing complete, it should be obvious by looking at the following code example that tests whether a given type represents a prime number:
Besides (and a necessary consequence of being turing complete), it is possible to create an endless recursion:
Turing completeness could be disabled, if it is checked that a type cannot use itself in its definition (or in a definition of an referenced type) in any way, not just directly as it is tested currently. This would make recursion impossible.
//edit: A proof of its turing completeness can be found here