Skip to content

Design Meeting Notes, 8/25/2023 #55511

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

Closed
DanielRosenwasser opened this issue Aug 25, 2023 · 5 comments
Closed

Design Meeting Notes, 8/25/2023 #55511

DanielRosenwasser opened this issue Aug 25, 2023 · 5 comments
Labels
Design Notes Notes from our design meetings

Comments

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Aug 25, 2023

Omitting prototype from keyof

#55471

export declare class SomeClass {
    static readonly FOO = "FOO";
    static readonly BAR = "BAR";

    // prototype: SomeClass;
}

export type ValueTypesOfClass = typeof SomeClass[keyof typeof SomeClass];

// This assignment should NOT be allowed, but it currently is in TypeScript.
const icon: ValueTypesOfClass = "hiiii";
  • What's wrong here?
    • Grabbing all of the types of values on typeof SomeClass - but that expands out to (typeof SomeClass)["prototype"] | "FOO" | "BAR".
    • But (typeof SomeClass)["prototype"] is just SomeClass, which is an empty instance type.
    • So it's really just {} | "FOO" | "BAR", and everything non-nullish is assignable to {}...
    • Which means that "hiiii" is assignable to ValueTypesOfClass
  • Pattern is very dubious - could've written an enum here.
  • Why get rid of it? It may break existing code.
  • Conclusion
    • Must check out Mongoose break. Believe it's an asymmetry between how we handle keyof and how we omit properties in mapped types. Want to see how fixing that up changes things.
    • But also, we want to be cautious. Very niche. Skeptial that it adds value, whereas there's always a risk of breaking.

Making const Contexts/Type Parameters Easier for Arrays

#51931
#55229

declare function f1<const T extends unknown[]>(arr: T): T;
declare function f2<const T extends readonly unknown[]>(arr: T): T;

const a = f1([1, 2, 3]);
//    ^? const a: unknown[]
const b = f2([1, 2, 3]);
//    ^? const b: readonly [1, 2, 3]
  • For f1 we infer unknown[] because it omitted readonly.

    • Why?
    • const pushes the type of the array towards readonly [1, 2, 3] - but that's not assignable to unknown[].
    • Array has methods for mutation, whereas ReadonlyArray does not.
  • Very annoying footgun - today, it makes no sense to write const type parameters with mutable array constraints in TypeScript.

  • Newest change pushes const-y element types without violating the expectations of the contextual type. In this case, it doesn't push the readonly-ness of the tuple as a whole.

  • Note the change isn't simply restricted to const type parameters - it also works with as const when an outer contextual type has a mutable array type - which means that a and b here are equivalent:

    declare function f1<const T extends unknown[]>(arr: T): T;
    
    const a = f1([1, 2, 3]);
    //    ^ [1, 2, 3]
    
    // ^
    // These work the same now!
    // v
    
    declare function f2<T extends unknown[]>(arr: T): T;
    
    const b = f2([1, 2, 3] as const);
    //    ^ [1, 2, 3]
  • Does this step on satisfies?

    • No, in fact we think it composes better. You can do as const satisfies unknown[].
  • It does break certain codebases which currently have a union of mutable/immutable tuple types. We think they can typically adapt the code fairly easily.

  • The most embarrassing part of the blog post introducing const type parameters was pointing out that you should never write const type parameters with mutable array constraints - but that they were allowed to be written.

  • Overall it feels right. Want to push on this direction.

Dynamically Named/Instantiable Tuple Labels

#44939
#55452

type SomeString = "world";

type Tuple = [`hello${SomeString}`: number]
// type Tuple = [helloworld: number]
  • Thought we understood this, took a closer look.
  • Why is it such a crucial need to relabel the parameters?
    • Seems dubious.
  • Implementation feels strange - the thing that instantiates and follows with the type, but disappears as soon as have a union.
  • Huh...
type SomeString = " world";

type Tuple = [`hello${SomeString}`: number]
// type Tuple = [hello world: number]
  • Oh no...

    type SomeString = ": number";
    
    type Tuple = [`hello${SomeString}`: number]
    // type Tuple = [hello: number: number]
  • OH NO.

    type SomeString = ": number, yadda";
    
    type Tuple = [`hello${SomeString}`: number]
    // type Tuple = [hello: number, yadda: number]
  • We know, we know this is a prototype. 😄 You'd need some escaping to handle those.

  • That's not the only reason we're iffy on this.

  • Can almost imagine wanting to support labels from const [count, setCount] = useState(0);

    • But that doesn't work. You'd need const [count, setCount] = useState<"count">(0);
    • Suggesting a type parameter for something entirely design-time feels a bit ridiculous.
  • Doesn't feel consistent with names in other positions - expression vs. types.

  • Don't like how you lose the name if you have a union - doesn't feel like it's consistent with the rest of the type system.

  • Feels like the code you'd need to write for this would be so gross it's not worth the feature.

  • @weswigham had alternative approach to the use-case in the original issue that doesn't even need dynamic names for tuples.

  • When it comes to design-time validation and help, open-ended string completions are something we should probably solve first.

  • Conclusion:

    • Number one thing is to better understand scenarios - we don't feel ready to commit to this until we see more motivating use-cases, code that this PR allows you to write that is compelling.
    • Skeptical of approach for implementation. Stuff to fix if we wanted the feature:
      • Syntax - make it consistent with where types are used over expressions in other positions. @weswigham to weigh in on this one.
      • Disappearing in union case - feels very odd for the rest of the type system. Not clear on what could change there.
        • [[Editor's Note: I don't know if that's a feature issue, but very surprising.]]
@DanielRosenwasser DanielRosenwasser added the Design Notes Notes from our design meetings label Aug 25, 2023
@tmm
Copy link

tmm commented Oct 4, 2023

Thanks for the detailed notes! Wanted to share a use-case related to "Dynamically Named/Instantiable Tuple Labels."

I maintain a popular library (ABIType) that allows you to convert JSON Application Binary Interfaces to their TypeScript representations for strong type-safety. For example the following ABI parameters:

[
  { "name": "foo", "type": "string" },
  { "name": "bar", "type": "uint256" },
  { "name": "baz", "type": "bool" }
]

Are converted into:

[string, bigint, boolean]

Using a utility type from ABIType: (Playground Link)

type Result = AbiParametersToPrimitiveTypes<
  // ^? type Result = [string, bigint, boolean]
  [
    { name: 'foo'; type: 'string' },
    { name: 'bar'; type: 'uint256' },
    { name: 'baz'; type: 'bool' },
  ]
>

ABIs can also describe functions. For example a function called "hello":

{
  "name": "hello",
  "type": "function",
  "stateMutability": "view",
  "inputs": [
    { "name": "foo", "type": "string" },
    { "name": "bar", "type": "uint256" },
    { "name": "baz", "type": "bool" }
  ],
  "inputs": [
    { "name": "zip", "type": "int8" },
    { "type": "int8" }, // <-- `"name"` is optional
    { "name": "qux", "type": "string[2]" }
  ]
}

Using ABIType, you can convert multiple ABI functions into a typed "SDK" that folks can use.

// converts list of ABI functions to typed `sdk` object
const sdk = createAbiSdk([...])

// can call each function with strong type-safety
const result = sdk.hello('world', 123n, true)
//                 ^? (method) hello(string, bigint, boolean): [number, [string, string]]

// as well as get a typed result
result
// ^? const result = [number, number, [string, string]]

This works really well, but would be even better if each of the inputs and ouputs had labels attached. For example:

hello(foo: string, bar: bigint, baz: boolean): [zip: number, number, qux: [string, string]]

ABI functions often have many inputs and outputs so generating dynamic labels alongside the types would be very useful for autocomplete, inlay hints, etc. Right now, folks need to refer back to the original ABI functions to remember which positional argument correspond to what names. Dynamic labels would eliminate this manual step.

Dynamic labels also seem like they could be useful for other popular libraries too, like tRPC, Drizzle, Pothos, ArkType. This could also be applied to future areas, like graphql queries/mutations type inference or arrays in HTTP responses (maybe someone wants to create an "ABIType"-like lib for OpenAPI). Basically anywhere you are using type-level programming to convert schemas to typed function representations.

This is my motivating use-case. Happy to answer any questions and thank you for discussing this.

@adamscybot
Copy link

adamscybot commented Apr 21, 2024

I'm also in need of this. In my case I'm adding types to a library that uses some special string sequence to effectively define a validator for function arguments. I have used TS template literals to parse this and great the arguments tuple. Some of the arguments that can be defined by this "special string" have specific behaviours that I would like to indicate in the label.

Because those behaviours can occur in multiple arguments, and the number of arguments is unbound, I can't do it, because I can't use the same tuple label twice and there is no way to generate something to disambiguate them.

@Yuripetusko
Copy link

Yuripetusko commented Aug 1, 2024

I'm sorry for tagging you @DanielRosenwasser, but I wasn't sure if you've seen the follow up context provided after your feedback, would appreciate at least a short acknowledgement and maybe an advice on how we can propose a more satisfactory solution that won't add the code to typescript that causes this:

make the code you'd need to write for this would be so gross it's not worth the feature.

Regarding the named/labeled tuples. You provided a really good feedback, mostly on this particular PR/proposal that also proposes "relabel parameters" and dynamic labels with string literals.

You also mentioned this in your feedback

Number one thing is to better understand scenarios - we don't feel ready to commit to this until we see more motivating use-cases, code that this PR allows you to write that is

@tmm followed up with a real world use case. Their libraries are used by thousands of projects. Directly as well as through their upstream libraries built on top of ABIType, including viem and wagmi. I am not sure if perhaps you missed his comment which in my opinion makes a really solid real world use case where named tuples are very much needed and not having a context of the tuple element names often is a source of bugs for consumers of their libraries and creates a lot of friction.

Would love to hear a further feedback with the use case provided by @tmm above if you ever find time to look at it. 🙏

@RyanCavanaugh
Copy link
Member

One use case is great! Usually when we're looking at new language features, we'd want to see something that would be used by a lot of different libraries. Right now we're not really seeing a ton of API surfaces that use tuples with dynamic semantics like this.

@Yuripetusko
Copy link

Yuripetusko commented Aug 1, 2024

One use case is great! Usually when we're looking at new language features, we'd want to see something that would be used by a lot of different libraries. Right now we're not really seeing a ton of API surfaces that use tuples with dynamic semantics like this.

Thanks for the answer, that makes sense, especially since this isn't actually a core functionality but rather an extra semantic metadata that and mostly needed for inlay hints etc. However also wanted to note that of course you are not seeing a ton of API using this feature since it's not available :D Well at least a dynamic labels are not, they work well if you explicitly define them, but not if you need a Generic or util to add them on the fly. I personally didn't even know I could do that until last week, and will be using it more now (except for dynamic part).

Just leaving this for the future reference if more folks will request this.

Currently ABIType, the library menioned above, has ~687k weekly downloads a week. And viem - (the main upstream library that utilises abitype) is doing ~560k downloads a week.

I'll try to spread the word about named tuples, because I suspect majority simply are unaware of them, then hopefully there's more voices than us, users of abitype/viem/wagmi, that will find out that they need it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Notes Notes from our design meetings
Projects
None yet
Development

No branches or pull requests

5 participants