-
Notifications
You must be signed in to change notification settings - Fork 0
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
Misc discussion thread #1
Comments
I'm just a random developer who enjoys typescript, cubing, and 3d printing.
const authorFrag = Author.$(
Author.id,
Author.name,
);
const bookFrag1 = Book.$(
Book.id,
Book.title,
Book.author.name,
);
const bookFrag2 = Book.$(
Book.id,
Book.author.$(authorFrag)
);
const bookFragCombined = Book.$(
bookFrag1,
bookFrag2,
);
gqx.query.getBook({ id: "abc" }, bookFragCombined).then(book => {
book; // Hover
/*
(parameter) book: {
__typename: "Book";
id: string;
title: string;
author: {
__typename: "Author";
id: string;
name: string;
};
}
*/
}) Playground Link (Very unresponsive to edits; I recommend throwing into a vscode tab) It also spews out lots of generic info about the graphql schema (both in types and runtime objects), which I've used in a couple projects to type tangental aspects of the gql schema, like resolver types. |
That looks beautiful! Have you ever experimented with TS-first approaches to schema definition as well? |
Thanks! If you're referring to type-level schema parsing, I haven't much. Though I very much like the concept theoretically, since it seems like they aren't going to support it, I think it might be more trouble than it's worth, especially since I prefer to have my schema in separate
|
@harrysolovay I thought this might interest you: https://github.com/dotansimha/graphql-typed-ast |
I guess this is to be our unofficial chat :) Apologies for the delay. Lots & lots of work. That repo absolutely did interest me! It was in fact a catalyst for my current project. I've been building a spec-compliant type-level GQL parser, and those recursion limits are brutal. Can barely parse a 30 loc schema without hitting limits. Also, the lack of throw types (proposed here) makes parse errors a pain: one needs to create a spec-incompliant node kind to contain errors, recursively extract all error nodes from the final tree, and display them with shady tricks ( Those HKT hacks you've been sharing seem promising! I find it hard to imagine that Ryan's comments against type-level parsers will stick... it would be such a beautiful way to tie together languages in a single env. One way or the other, 4.1 will be of huge benefit to GraphQL-in-TS tooling. I can see you've been thinking about the same problem space. Let's post in this issue whenever we want feedback from one another. What've you been hacking on? Any cool issues I should take a look at? |
Also, any thoughts on this issue? I feel like we're still writing conditional types like cave people ;) |
@harrysolovay Yeah, I saw it the other day and I agree that it would be nice. I started to write up a comment for it, but it must have ended up unsent among my tabs 😄 |
Oh, I didn't see your earlier comment. Yeah, throw types are a must for complex recursive conditional types, though IMO they should be assignable like The problem that I've been running into with the HKT hacks is that if you have an HKT that increases the depth by e.g. 10, you have a lot less depth to work with than one that increases the depth by e.g. 1. I've been thinking about if there would be a way for it to detect that it hit depth limit (sometimes if the recursive type is wrapped, the error doesn't propagate and just returns I've recently been playing around on the discord with a type Axiom<A> = Assert<ConvertibleTo<A>, ConvertibleTo<ConvertibleTo<A>>> Eventually I got to this, but it would be nice to be able to do it in a more general way. Wrt interesting issues, I personally think microsoft/TypeScript#41370 is a pretty big type hole, though I may be biased :) |
Wow! That is absolutely a hole. Surprised I haven't encountered this limitation before. I like that ... which has me thinking... we need conditional assignability, badly. |
Yeah, that's exactly what I'm using/developing it for :) Yeah, we really do. |
Also useful for the trait-style lib is what I've been using in my project to handle plugins: // Main library
declare global {
namespace someLibrary {
interface Conversions {
aToB: Conversion<A["id"], B["id"]>
}
}
}
// In some plugin
declare global {
namespace someLibrary {
interface Conversions {
myPlugin: {
bToA: Conversion<B["id"], A["id"]>
misc: (
| Conversion<B["id"], C["id"]>
| Conversion<X["id"], Y["id"]>
)
}
}
}
} |
This might take me a little while to grok. Is |
That was an alternative for my: type Conversions =
| Conversion<A["id"], B["id"]>
| Conversion<B["id"], A["id"]>
| Conversion<B["id"], C["id"]>
| Conversion<X["id"], Y["id"]> AFAIK, unions cannot have multiple declarations / be extended. Instead, I use an interface, and then to form the union of all conversions, I deeply get the values of the interface: type ConversionsUnion<C = someLibrary.Conversions> = {
[K in keyof C]: C[K] extends Conversion<any, any> ? C[K] : ConversionsUnion<C[K]>
}[keyof C] With this, plugins can add conversions by extending the global interface The keys are irrelevant, and just have to be unique so they don't conflict between plugins; I allow it to be deep just for easier namespacing. |
|
I see! How do you––at the type-level––safeguard against plugins providing conflicting conversions for the same to types? (I'm assuming there's a runtime component which injects properties in the module being augmented?) |
Right now, I don't have any type-level safeguards against conflicting conversions; all I care about in the type system right now is whether or not some path exists from Run time wise, I have a registry for the conversions, and then it essentially path finds from
What do you mean? |
This approach of yours is just spectacular. Hopefully it'll enable––at the very least––type-level GQL document parsing to work for production apps (even if the schema types still need be generated). |
Is that why you chose to give the conversions keys? (so that users need explicitly select their desired conversion?) |
Would you possibly be able to write out an end-to-end from the user's perspective? It's still looking quite foreign to me. |
Did this comment of yours mean that the HKT solution is not viable for circumventing recursion limiting in building string type parsers? |
No; the keys are completely irrelevant. The end user doesn't really care about conversions (except for library authors); for the most part, it Just Works™
Some context: I'm writing a programmatic 3d modeler (similar to OpenSCAD, but That's somewhat difficult, as the end user doesn't ever really care about the details of conversions etc. unless they are writing a lower level library, but here's an example of the logic. // myModel.ts
import escad from "escad"
import { cube } from "@escad/cube"
export default () => {
return escad
.cube({ sideLength: 1, center: true })
.translateZ(1)
.sub(cube({ sideLength: 2, center: true }))
} Ignoring chaining implementation details: interface Cube extends Product { /* ... */ }
interface TransformationMatrix extends Product { /* ... */ }
interface Mesh extends Product { /* ... */ }
interface BspNode extends Product { /* ... */ }
interface CsgOperation extends Product { /* ... */ }
type Transformation<P extends Product> = CompoundProduct<[TransformationMatrix, Cube]>
type CsgOperation<P extends Product, Q extends Product> = CompoundProduct<[CsgOperation, P, Q]>
cube // (args: CubeArgs) => Cube
translateZ // (z: number) => <P extends Product>(p: P) => Transformation<P>
sub // <P extends Product>(p: P) => <Q extends Product>(q: Q) => CsgOperation<Q, P>
import myModel from "./myModel"
const output = myModel() // CsgOperation<Transformation<Cube>, Cube>
type Assert<T, U extends T> = U;
type _0 = Assert<ConversionsUnion, (
| Conversion<Cube, Mesh>
| Conversion<Transformation<Mesh>, Mesh>
| Conversion<BspNode, Mesh>
| Conversion<Mesh, BspNode>
| Conversion<CsgOperation<BspNode, BspNode>, BspNode>
)>
// Thus,
type _1 = Assert<ConvertibleTo<Mesh>, typeof output>
// Because:
// CsgOperation<Transformation<Cube>, Cube>
// CsgOperation<Transformation<Mesh>, Cube>
// CsgOperation<Mesh, Cube>
// CsgOperation<BspNode, Cube>
// CsgOperation<BspNode, Mesh>
// CsgOperation<BspNode, BspNode>
// BspNode
// Mesh This might seem over complicated, but it's useful for a number of reasons:
When I tried using it with the HKT recursion circumvention, it was 2589ing very quickly. I likely could have tweaked it to not 2589, but I wasn't really in the mood :) |
Haha I know that feeling: it's gotta be a labor of love. The manic, unpredictable kind. That's a very elegant experience you're working on! Once again, a lot to grok, but I feel the essence & I'd imagine users will appreciate the thoughtful typing experience. Extraordinary how much one can achieve with TypeScript's type system nowadays. Out of all the coders I've met, the most sophisticated––when it comes to type systems––were once big into cubing. Funny coincidence I suppose. Or who knows, maybe some common experiences guided us to this niche. If you do manage to circumvent 2589, please let me know. Although it looks like you have your hands full. Also, feel free to add me as a reviewer if you ever want fresh eyes on your projects. Looking forward to seeing what escad becomes––even if it's purely research 👍 |
Thanks! Once it gets further along and I clean it up a little, I'd love to hear your thoughts on it! |
Here a "sneaky type" I made; when typescript is checking its constraint with the type parameter I'm unsure if this has any uses, but it's an interesting edge case. |
What do you think about a syntax like the below? type [infer A, infer B] = [1, 2]; Where that would be equivalent to: type A = [1, 2] extends [infer A, infer B] ? A : never;
type B = [1, 2] extends [infer A, infer B] ? B : never; I think this would be especially useful in the context of microsoft/TypeScript#41470; the example I provided in there could be rewritten to: type PromisifyAll<T> = {
[K in keyof T & string as `${T}Async`]: {{
type ((...args: infer OrigArgs) => void) = T[K];
type [...infer Args, (error: any, result?: infer R) => void] = OrigArgs;
type = (...args: SplitArgs["args"]) => Promise<SplitArgs["result"]>;
}}
} Which IMO looks very clean. If it could be generic, you could nicely rewrite some common type aliases: type [infer Head, ...infer Tail]<T extends any[]> = T; As opposed to: type Head<T extends any[]> = T extends [infer X, ...infer _] ? X : never;
type Tail<T extends any[]> = T extends [infer _, ...infer X] ? X : never; |
I love the look of it! It's parallel to destructuring. Reminds me a bit of this issue, although I don't think the proposal was as strong as yours would be. It would be useful for type parameters as well. (example from issue) type A = {
B: {
C: [
{D: "E"}
]
}
}
type X<{B: {C: {[infer D]}}} extends A> = D; How would one deal with conditional destructuring / potentially-missing fields. Would it make sense for––upon a destructured element containing
I don't quite understand how you would use this:
Seems like you'd wanna do the following instead: type HeadAndTail<T extends [infer Head, ...infer Tail]> = [Head, Tail];
type [infer Head, infer Tail] = HeadAndTail<["a", "b", ""c]>;
// or better yet, don't even use a utility
type [infer Head, ...infer Tail] = ["a", "b", ""c]; I like the anonymous type scoping proposal! As of today, I use namespaces to keep tidy... anonymous scopes could be of great help with that. Will comment in that thread soon. Also apologies for not responding to your sneaky type! I hadn't yet had the time to understand it. Looks fascinating though. |
Hadn't thought of that, though I've always wanted some way to do that. Though when I've thought about something like that, in my mind it has always been something like your type X<_ extends A & {B: {C: [infer D]}}> = D;
I had actually been thinking about this; I was initially thinking about I think it could be nice if typescript threw an error if it couldn't prove that the infer pattern would match; if you wanted to to fall back to never, you could use type [infer A, infer B] = [1]; // Error
type [infer A, infer B] = [1] || never; // Ok, A & B are both never
In my mind, that would be equivalent to: type Head<T extends any[]> = T extends [infer Head, ...infer Tail] ? Head : never;
type Tail<T extends any[]> = T extends [infer Head, ...infer Tail] ? Tail : never; While somewhat strange for that case, I think allowing generic support could be nice, to allow something like type { x: infer X }<T> = Y<T>; Instead of type X<T> = {{ type { x: infer X } = Y<T>; type = X }}; or type X<T> = Y<T> extends { x: infer X } ? X : never; But perhaps it's not clear enough from the declaration that
No problem! I probably should have given some explanation of it. Essentially, I made type Sneaky<$, V=$, F=never> = Unwrap<(Wrap<F> & ($ extends infer _ ? Wrap<V> : never))>; |
That's quite an edge case you've found! Very cool! Unrelated: Since 4.0, I'm wondering when stricter generator types will happen. Generator signatures should IMO contain the ordered sequence of yields, next args, & return. This would enable library developers to create type-safe generator-based experiences, wherein the user supplies a to-be-type-checked generator. For instance, let's say I want to mimic inheritance: a generator would allow me to import {toConstructor, useBase} from "generator-to-constructor";
// To inherit from:
interface CowProps {
spotted: boolean;
}
class Cow {
spotted;
constructor(props: CowProps) {
this.spotted = props.spotted;
}
moo() {
console.log("MOOO!");
}
}
// Synthesizing a constructor:
interface SmartCowProps extends CowProps {
name: string;
}
const SmartCow = toConstructor(function*(props: SmartCowProps) {
const scope = yield useBase(Cow, {spotted: props.spotted});
scope.moo();
return {
name: props.name,
}
}); You could even allow import {toConstructor, useBase, Instance} from "generator-to-constructor";
interface CowProps {
spotted: boolean;
}
function* Cow(props: CowProps) {
yield; // doesn't inherit anything
return {
spotted: props.spotted,
moo() {
console.log("MOOO!");
},
}
}
interface SmartCowProps extends CowProps {
name: string;
}
function* SmartCow(props: SmartCowProps) {
const scope = yield useBase(Cow, {spotted: props.spotted}); // essentially a `super` call
scope.moo();
return {
name: props.name,
}
};
const smartCow = Instance(SmartCow, {
spotted: true,
name: "Rick Ross",
}); Type-checking this would be crucial, as we're expecting a single yield containing a constructor and that constructor's props, and then a return which is to be instance members. I'm curious to hear your thoughts on generator strictness, as well as the above. Also––I feel like this is a topic you'd enjoy: any ideas about what a macros system for TS would look like? Was sifting through issues and though of maybe submitting a proposal in this one. |
Ahh, ya got me ;) |
Gave ya a shoutout in this article that I just published. |
That was a fun read! Sorry to ruin your theory, but I am not Elvis on a private island 😆 . How did the Also, I realized a way type level parsing might be able to perf well (assuming it didn't 2589): build mode with project references will only build it when it changes. (Though thinking about it now it might not actually instantiate the type before it generates the d.ts) Reading through your article gave me an idea for possibly circumventing 2589 more legibly. I'll report back soon 🙂 . (Edit: just sticking this here so I can find it) |
Not sure what to say, so I'm just gonna link this and let it speak for itself. 😄 |
Not Elvis... dang ;) The project references idea is interesting. Would every sub-parser be its own package? And how would this be more performant? I'm not so sure I fully understand the link. Are you suggesting there's an approach that would make type-level GraphQL parsing feasible? Also, I'm glad you enjoyed the read! |
In my mind, the project references would be more performant because dependents only get built on change, but thinking about it now, the generated d.ts might leave the I was trying to demonstrate a recursion limit hack; given a tail recursive type like below: // type Final = "x".repeat(1e3)
type LotsOfX<T extends string = ""> = T extends Final ? T : LotsOfX<`x${T}`>; You can transform it into: type _LotsOfX<T extends string = ""> = T extends Final ? Result<T> : Defer<LotsOfX<`x${T}`>>;
type LotsOfX<T extends string = ""> = x1e9<_LotsOfX<T>>;
// "x".repeat(1e3)
type X = LotsOfX; In that example, it went through 1000 iterations of 'T = In retrospect, this is probably a better example. At that point, the challenge is to make the namespace Patterns {
export type Leaf = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
export type Element = Leaf | Group;
export type Group = ["(", Element[], ")"]
}
// Parses without 2589
type X = Parse<Patterns.Element, "((1)((((0))))(1(0101(((1(((((1)))))()()()(0001234567890()0000)1)))01010((10)1)010)1))">
I'm going to be modifying it to support a mapping at the end so that it can produce a normal-looking ast. |
With a bit more nesting, we get the following. Writing these type-level parsers is fun... but it's also such a limited process. That conditional assignability syntax would make a difference to this process. Even if we had it though, a plugin system for intrinsics might be preferable. While I love the problem of type-level parsing, it doesn't seem like we're quite there yet :/ On another note, I'm surprised no one else has chimed in on the documentation utility type discussion. Seems like it'd be an extraordinary addition to the language. & how is the 3D modeling lib coming along? |
😞
Agreed.
It's going well. I'm currently procrastinating making the front-end, but the core model generation / conversion stuff is mostly done. I have yet to put it through a real stress test, however. |
Update: stress test did not go well. Will need further testing 😆 I think it might have been in an infinite loop, but maybe my algorithm requires 3.5+ million iterations 🤷 |
Haha sorry to hear. Hopefully you don't let it loose on an AWS lambda function with self-provisioning permission ;) Looking forward to seeing / hearing about whatever it becomes |
This inconsistency is annoying. |
Very interesting! Likely the same issue where inference breaks upon unwrapping known types in conditionals. Probably worth an issue in TypeScript. Is there any reason that this behavior would be desirable? |
I think that's a separate issue; there it doesn't infer the constraint of the type (though if you ignore the error, it works). With the one I linked, it doesn't infer it properly in the end result either. I guess the inference of conditionals is separate from inference of function arguments? |
If you got your hands on a type X<A> = <B> = [A, B];
type Y = X<number>; // <B> = [number, B];
type Z = y<string>; // [number, string]; |
A |
Something like this?: type Call<
G extends <X> = any,
A extends (G extends <infer X> ? X : never)
> =
G<A>; |
IMO the I would probably opt for something like type CallHKT<
G extends <X extends never> => unknown, // These could also be any, but this demonstrates the variance
A extends (G extends <X extends infer X> => unknown ? X : never)
> = G<A>; Which would IMO reduce that confusion. But, no, when I say "a type Callable = (...args: any[]) => any;
type CurryOnce<T extends Callable, F extends <X> => any = <X> => X> =
| ([] extends Parameters<T> ? () => F<Call<T, []>> : never)
| (
<A extends HeadArr<Parameters<T>>>(...args: A) =>
F<
<B extends Tail<Extract<Parameters<T>, [...A, ...any[]]>>>(...args: B) =>
Call<T, [...A, ...B]>
>
)
type Curry<T> = (
T extends Callable
? T extends () => infer R
? R
: CurryOnce<T, Curry>
: T
)
declare const curry: <F extends Callable>(f: F) => Curry<F>;
declare const make3Tuple: <A, B, C>(a: A, b: B, c: C) => [A, B, C];
// <A>(a: A) => <B>(b: B) => <C>(c: C) => [A, B, C]
const curriedMake3Tuple = curry(make3Tuple); |
I see what you're saying! Such utilities for partial application would enable HOTs as well if I'm not mistaken? |
Yes; with a |
I wanted to check in. I hope you're enjoying this holiday season. What've you been hacking at lately? Thought I'd update you as well: I'm switching teams. Joining Alexa, where I'll be working on a programming language for the conversation-modeling use case. You and I have had great conversations about language, and I'd love to get your feedback on design decisions down the line. It's all still very new to me, and I haven't started yet, so I can't detail the work with any certainty... but I'm excited! Meanwhile, I've been writing an absurdly-fast GraphQL-to-TS-declaration generator in Rust. This project is helping me learn the language. The challenge of minimizing memory footprint in Rust feels similar to that of maximizing type-safety in TS. Speaking of TS, I've been very insistent on generic type safety for tagged templates––love to hear your thoughts on the issue. & yea, that sums it up. This new year is looking up man 💯 |
Ditto! Not too much, mostly advent of code (1st on the TypeScript Discord leaderboard 😄 ) and migrating my Rubik's cube simulator into unity, with the intention of adding VR support.
That sounds neat; I'm looking forward to hearing more about it!
Huh, I never fully realized there wasn't generic support there. I'll probably write up something in the next day or two, though I think your comments cover a bit of what I'd say :) |
Neat! Rust has been a language I've been meaning to learn for a while; if you're ever looking for another set of eyes, feel free to send me a link :) |
I'll absolutely share it with you when it's ready. Just joined the TypeScript discord. Advent of Code looks interesting! What specifically have you been diggin' into with regards to Advent of Code? |
Cool! I'm T6 on there. I completed all the challenges (you can see my solutions at tjjfvi/aoc), but the most interesting one is probably my solution for day 19, part 2. I can go into more detail later, but it uses a generic, type-safe, generator-based parser I made that I'm quite fond of. Here's an example of a JSON parser using it (not very polished/readable). It doesn't make much use of its type-safety (it casts it to any because it's spitting out raw JSON); I can make a better example later if you're interested. |
This is unbelievably cool! I don't have the time right now to dig in fully, but it looks fancy! When you say it's type-safe... you don't mean you're getting the parsed result as a literal type, do you? |
Thanks!
Not a problem! I hope you don't mind if I explain it some here anyway :) It's based off of generator functions (aptly named Here's a simple example: const leaf = regex(/\d/);
const group = concat(string("("), multiple(() => element), string(")"));
const element = or(leaf, group); This is great, but the return type of this isn't very useful, as it would look something like // We use ([n]) to unwrap the RegExpExecArray
const leaf = map(regex(/\d/), ([n]) => +n);
const group = map(concat(string("("), multiple(() => element), string(")")), ([,els]) => els);
const surround = <R,>(before: Parser<unknown>, parser: Parser<R>, after: Parser<unknown>) =>
map(concat(before, parser, after), ([,v,]) => v)
const group = surround(string("("), multiple(() => element), string(")")); Right now, this will give a type error, since typescript can't determine the type of circular variables. We have to give type Element = number | Element[];
const element: Parser<Element> = or(leaf, group); In the end, it looks like this ( type Element = number | Element[];
const leaf = map(regex(/\d/), ([n]) => +n);
const group = surround(string("("), multiple(() => element), string(")"));
const element: Parser<Element> = or(leaf, group);
console.log(parse(element, "(01234(5)(6()7)89(10))")) // [0, 1, 2, 3, 4, [5], [6, [], 7], 8, 9, [1, 0]] Some more examples: JSON parser (cleaned up), expression parser, infix to postfix (a minor adaption of the expression parser). All of these do the processing in
No; I wish :). I mean that if you use it to construct an AST (or even my barebones |
Hey! Would love to check in. Are you around for a call sometime? |
I'm on discord a bit; I've sent you a friend request there if you want to chat more synchronously 🙂 |
Sounds good! I can't seem to find the request––what's your username? |
I'm T6#2591 |
I've enjoyed our chats in TypeScript issues. I see you're also a "cuber" (cool simulator!). Gqx looks auspicious as well.
You don't list a name, Twitter handle, etc. anywhere on your GitHub profile or project manifests.
Perhaps this is an unusual request, but I'd love to know who is behind the screen & make sure to stay posted. If you'd prefer to stay anonymous, I completely understand & respect your wishes!
Kind regards
The text was updated successfully, but these errors were encountered: