Skip to content

Design Meeting Notes, 10/18/2023 #56158

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 Oct 19, 2023 · 7 comments
Closed

Design Meeting Notes, 10/18/2023 #56158

DanielRosenwasser opened this issue Oct 19, 2023 · 7 comments
Labels
Design Notes Notes from our design meetings

Comments

@DanielRosenwasser
Copy link
Member

Varying Types for Index Signatures and Mapped Types

#43826

  • Today, TypeScript supports separate read and write types.

    interface Foo {
      get someValue(): string;
      set someValue(someValue: number);
    }
  • Some feedback requesting the ability to do this with index signatures and mapped types.

  • How do we feel about the idea?

    • Don't like need to repeat the [K in keyof T] for each get and set type for mapped type.

      • Would need to say "the forms must be the same".
    • Another proposal - grouped get and set for mapped types.

      type Foo<T> = {
        [K in keyof T] { // wait, what if people accidentally put a colon here? or forget one?
            get(): T;
            set(value: T[K] | undefined);
        }
      }
    • Okay, how about the accessor keyword?

      type Foo<T> = {
        accessor [K in keyof T] {
            get(): T[K];
            set(value: T[K] | undefined);
        }
      }
  • Use case is a map that's "optional on the write, always present on the read"

  • Do the names have to be the same between get and set index signatures?

  • get vs. readonly index signatures?

  • Do other properties get validated against set index signatures?

    • Maybe not?
    • But what if you have 3 properties declared with get/set?
  • What are the use-cases?

    • Runtime type validation functionality in Fluid Framework.
    • Vue 3 refs/reactives
      • But how?
      • Unclear - what in TypeScript is lacking from the given examples? We don't see a difference in read/write types.
  • Conclusion?

    • We're not entirely opposed to the feature, but we are wary of the complexity involved here. We would like to better understand the use-cases.

Unrelated Interface Causes Assignability Differences

#56099

export { }
interface Map<V> extends Collection<V> {
    flatMap<VM>(): Map<VM>;
}

interface Collection<V> {
    value: V; // sprinkle some covariance
    map: Map<V>;
    concat(): Collection<unknown>;
    flatMap(): Collection<V>;
    flatMap<VM>(): Collection<VM>;
}

// Comment out and it works like in 5.2
interface Keyed extends Collection<number> {
    concat(): Keyed;
}

type R = Map<never> extends Collection<infer V> ? V : "NO";

const r = null! as R;
const t: "NO" = r; // should *not* succeed!
  • Explanation at Unrelated interface causes assignability to change  #56099 (comment)
  • When comparing types, we store each level on a stack.
  • When we detect that types are generative by seeing type IDs increase, we figure "these are deeply nested" and say "yes, they are sufficiently related".
  • When performing inference, we only go 1 level deep and then stop if we re-encounter a higher type ID.
    • We could go deeper, but we don't want to spend a whole bunch of time in inference by increasing this length check.
  • Sometimes a type with a higher ID will materialize indirectly!
  • Another thing - any is not assignable to never - kind of surprising.
    • Feels like a subtype versus assignability thing.
  • Other places this doesn't quite work out - TypeScript ignoring checks for deeply nested computed object types #56138
  • Something we could say with the type arguments themselves?
    • The type arguments are not necessarily interesting, it's what you do with them.
  • The type ID fix has been largely good, but there are some imperfections.

DefinitelyTyped is a Monorepo!

DefinitelyTyped/DefinitelyTyped#67085

  • DefinitelyTyped is a proper monorepo!
  • Every package has a package.json with versions and stuff!
  • pnpm creates self-links - no path mapping from tsconfig.json!
  • Dependencies are now made explicit (e.g. @types/node)
    • e.g. built-in Node.js events versus npm library events
  • pnpm, other than great performance, allows us to have the packages at different versions!
  • CI is still slow, still other improvements that are ongoing.
@DanielRosenwasser DanielRosenwasser added the Design Notes Notes from our design meetings label Oct 19, 2023
@fatcerberus
Copy link

fatcerberus commented Oct 20, 2023

Another thing - any is not assignable to never - kind of surprising.

This was always surprising to me too, because any is supposed to be the "don't typecheck this" type. However I'm also pretty sure it's explicitly documented in the release notes of whatever TS version added never which suggests it was fully intentional

@MartinJohns
Copy link
Contributor

#8652

No type is a subtype of or assignable to never (except never itself).

@pikax
Copy link

pikax commented Oct 20, 2023

Vue 3 refs/reactives

  • But how?
  • Unclear - what in TypeScript is lacking from the given examples? We don't see a difference in read/write types.

Vue 3 refs/reactives provide automatically unboxing of deep refs:

const a = reactive({ foo: 1 })

a.foo // 1


const foo2 = ref(2)
// valid
a.foo = foo2;

a.foo // 2

a.foo = 3

foo2.value // 3

typescript playground
vue playground


In regards to the options, I think either no colon and accessor would be fine.

type Foo<T> = {
  [K in keyof T] { // wait, what if people accidentally put a colon here? or forget one?
      get(): T;
      set(value: T[K] | undefined);
  }
}

If they forget the colon it becomes an object with 2 function properties :D

@DanielRosenwasser
Copy link
Member Author

DanielRosenwasser commented Oct 20, 2023

I see, so you want to permit a write of a T | WrapppedThing<T>, but a read should always appear as a T - is that correct @pikax?

Would it be described as something like the following?

type Ref<T> = {
  [K in keyof T] {
      get(): T[K];
      set(value: T[K] | Reactive<T[K]>);
  }
}

@pikax
Copy link

pikax commented Oct 20, 2023

@DanielRosenwasser exactly

@DanielRosenwasser
Copy link
Member Author

(I've amended my comment to have an example of the type itself)

@pikax
Copy link

pikax commented Oct 20, 2023

@DanielRosenwasser roughly that, naming would be a bit different, I think it could be described like this:

// ref value is access via .value, to allow assign primitives
type Ref<T> = { value: T } 

// Unbox ref
type Unref<T> = T extends Ref<infer R> ? R : T;

type Reactive<T extends Record<PropertyKey, any>> = {
  [ K in keyof T] {
    // we need to deep unboxing the type
    get(): T[K] extends object ? Unref<Reactive(T[K])> : T[K];
    set<V extends Unref<Reactive<T[K]>> | Reactive<T[K]>  |  Ref<T[K]>>(value: V): V; // we return the same value that was set
  }
  
  
// example
const msg : Ref<string> = ref('Hello World!')
const a: Reactive<{ msg: string }> = reactive({ msg })

console.log(a.msg = ref('random')) // ref('random')
console.log(a.msg) // "random"

That's the current behaviour at runtime. playground

While building this might be useful to allow the set to be a generic, to be able to use it as return value, also allowing overrides to prevent having many conditional types.

EDIT: Made a few edits on the code, this is to show the object is set to Reactive must be allow deep set Reactive/Ref/regular object, the Reactive will always unbox Ref<T> to be T regardless how deep the property is on the object.
That causes the example to be a bit more complicated for an example, here's the simple version (which does not cover all of our use cases, because of the get is not deep unref properties):

type Reactive<T extends Record<PropertyKey, any>> = {
  [ K in keyof T] {
    // in case the T[K] is a ref
    get(): Unref<T[K]>;
    set<V extends Unref<T[K]> | T[K]  |  Ref<T[K]>>(value: V): V; // we return the same value that was set
  }

EDIT2: Not a Vue use case AFAIK, but supporting overrides might be useful, although besides possibly making the set function cleaner, not sure what else it can be used for, maybe someone knows a use case

type Foo<T> = {
 [ K in keyof T] {
   get(): T[K]
   // set overloads
   set(value: Unref<T[K]>): Unref<T[K]>
   set(value: Reactive<T[K]>): Reactive<T[K]>
   set(value: Ref<T[K]>): Ref<T[K]>
   // etc
 }
}

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