Skip to content
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

Design Meeting Notes, 7/28/2017 #17502

Closed
DanielRosenwasser opened this issue Jul 29, 2017 · 6 comments
Closed

Design Meeting Notes, 7/28/2017 #17502

DanielRosenwasser opened this issue Jul 29, 2017 · 6 comments
Labels
Design Notes Notes from our design meetings

Comments

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Jul 29, 2017

f# Readonly on Exports from Namespaces (#16720)

namespace N {
    export class C { }
}

N.C = class {};
  • Namespaces have had this feature for a while now
    • Breaking changes for something that's functioned that way this long seems strange.
  • Declined

Readonly & Variance

  • One thing that comes up quite a bit is strict variance.

  • If we enabled stricter variance, that would imply that Array becomes invariant.

    • To get a covariant array, you need a ReadonlyArray.
    • This means everything gets two declarations - T and ReadonlyT
  • Strict variance is easy to enable, but would be hard to make easy to use.

  • This leads us to start thinking about how to quickly make a readonly version of a type.

  • We've had an experimental type operator called readonly

    • Much like keyof T, you just write readonly T.
  • Like the Readonly mapped type, it makes all properties readonly as well.

    • However, the Readonly<T> mapped type doesn't eliminate mutating methods at all.
  • This means we needed to add a way to indicate that a method doesn't mutate its object.

    • Created the this: readonly syntax.
  • Example:

    class Foo {
        name: string;
        getName(this: readonly): string;
        setName(value: string): void;
    }
    
    // This:
    type ReadonlyFoo = readonly Foo;
    
    // ... is equivalent to this:
    type ReadonlyFoo = {
        readonly name: string;
        readonly getName(this: readonly): string;
    }
  • What about deep readonly?

    • In other words, does readonly Foo[] give you a readonly Array<readonly Foo>?
    • Have thus far decided no.
      • Could create an immutable type operator
        • Q: Couldn't you implement that with mapped types?
          • A: Potentially
        • May be a bit confusing since state could be captured outside of current object
  • In --strictReadonly mode, this readonly operator would work as described above.

    • readonly T not assignable to T.
    • readonly props would not be assignable to mutable props.
    • readonly T removes mutating methods
  • Outside of strictReadonly, readonly T is just Readonly<T>

  • All of this means that library authors would need to enable readonly mode.

  • Would TypeScript be able to infer whether a method is readonly or not?

    • Kind of tricky, methods transitively call each other.
  • How bad are strict variance modes without this?

    • Special hell for passing callbacks.
    • Need a super constraint for generics - otherwise many types stay invariant by virtue of taking a T as an input position.
  • Will people know how to use this: readonly?

    • Seems like people will always need to be mindful of this when they write .d.ts files.
    • How will we generate this for the DOM if the metadata isn't present? What about readonly HTMLElement?
  • How do this: readonly parameters work with overloads?

    • Currently, if any overloads are not declared with this: readonly, the property is eliminated.
    • That's not going to play well with libraries.
      • Example: Knockout.
@Igorbek
Copy link
Contributor

Igorbek commented Aug 1, 2017

Since there's no issues linked to 'Readonly & Variance' section (you may want to link #1394, #10717), I'll leave a few comments here.
I'm wondering why a connection mentioned between variance and readability in that way. My understanding is that a type may be co- or contravariant on some other type it uses. Like a function is covariant on its parameter argument types and contravariant on return type. In case of array, it may be covariant to type of its elements, but it does not imply that it must be read-only.

I claim that in some cases, it is not obvious or even impossible to be express variance with readonly T. Consider:

interface Transformer<I, O> {
  transform(value: I): O;
  transformArray(values: I[]): O[];
}

What would readonly Transformer<I, O> mean? How is that useful for variance? More importantly is that Transformer is covariant on I and contravariant on O.

In #10717 I was proposing both co- and contra- varainces on generic types for both declaration and use sites:

interface Array<T> {
  [index: number]: T;
  push(value: T): void;
  pop(): T;
}
type ReadonlyArray<T> = Array<out T>;
// is equivalent to this
interface ReadonlyArray<T> {
  readonly [index: number]: T;
  // push(value: T): void;
  pop(): T;
}
type WriteonlyArray<T> = Array<in T>;
// is equivalent to this
interface WriteonlyArray<T> {
  writeonly [index: number]: T; // no such syntax
  push(value: T): void;
  // pop(): T;
}

@DanielRosenwasser
Copy link
Member Author

DanielRosenwasser commented Aug 1, 2017

@Igorbek I've been thinking more about use-site variance recently as well. @ahejlsberg will be able to give more direct context, but I'll try answering your questions.

In general, the connection is that the readonly modifier removes every single method that isn't marked as this: readonly, which removes a great deal of methods that tend to make types invariant. While there's sometimes cases where generics in an input position don't necessarily modify contents of the containing type, this seems to be the case for a decent number of APIs. I believe the overall intent is that for many cases, the readonly modifier would make it significantly easier to manufacture a type that acts covariantly on its type parameters, which comes up quite a bit.

As an example, look at how we've had to create ReadonlyArray as a restricted counterpart for Array. You'd presumably want a ReadonlySet as well. Instead of declaring everything twice, we're exploring a more generalized mechanism.

To be clear, we are still experimenting with these ideas. It's not even clear whether stricter variance is something we're committed to, but I'm glad you asked.

@DanielRosenwasser
Copy link
Member Author

DanielRosenwasser commented Aug 1, 2017

Edit: I just noticed you pointed part of this out when you wrote:

In case of array, it may be covariant to type of its elements, but it does not imply that it must be read-only.


By the way, I want to point out that for many containers, a Foo<out T> is not necessarily read-only (imagine a method named clear()).

So neither readonly-ing a Foo<T> nor passing an upper-bounded existential type argument (Foo<out T>) fully subsumes the other.

@Igorbek
Copy link
Contributor

Igorbek commented Aug 1, 2017

Thank you @DanielRosenwasser for answering. That actually was my point that they, read-only-fity and covariance, are not always connected.
In my example, I just wanted to show something with current syntax. In future syntax it would be something like:

interface Transformer<I, O> {
  transform(value: I): O;
  transformArray(values: in I[]): out O[];
}
// or, as a shortcut for the same
interface Transformer<in I, out O> {
  transform(value: I): O;
  transformArray(values: I[]): O[];
}

, or, in a more simplified case, I could just mark only I and let compiler do other work:

interface Transformer<I, O> {
  transform(value: I): O;
  transformArray(values: in I[]): O[];
}

declare const t1: Transformer<string, 'a'>;
const t2: Transformer<'b', 'a'> = t1; // ok
const o2 = t2.transformArray(['b']);
o2.push('a'); // ok

const t2: Transformer<string, string> = t1; // allowed, but! O is implicitly out
const o2 = t2.transformArray(['b']);
o2.push('b'); // error, method pushed is excluded

(kinda random thoughts)

@aluanhaddad
Copy link
Contributor

Just 🚲:house:ing, but while I know TypeScript has been sticking to any established JavaScript precedents, wherever available, when adopting type level syntax (extends/implements), C#'s in and out really are so much more expressive and easier to visually parse than <? super T> and <? extends T>, so if explicit variance annotations are ever added I think it might be nice to deviate and use in and out as @Igorbek does in his code examples above.

@mattmccutchen
Copy link
Contributor

I propose that we continue the discussion of "Readonly & Variance" in #18770.

@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Design Notes Notes from our design meetings
Projects
None yet
Development

No branches or pull requests

4 participants