Skip to content

Design Meeting Notes, 8/25/2023 #55511

@DanielRosenwasser

Description

@DanielRosenwasser

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.]]

Metadata

Metadata

Assignees

No one assigned

    Labels

    Design NotesNotes from our design meetings

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions