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

Subset types for easier updating of immutable structures #10803

Closed
markboyall opened this issue Sep 9, 2016 · 7 comments
Closed

Subset types for easier updating of immutable structures #10803

markboyall opened this issue Sep 9, 2016 · 7 comments
Labels
Duplicate An existing issue was already created

Comments

@markboyall
Copy link

The core use case for this is updating immutable objects. Currently, this can't really be typed properly in TypeScript. Consider

interface MyObject {
property1: number;
property2: number;
}

obj: MyObject = ...;

obj = merge(obj, { property1: 1 });

merge is currently untypable in Typescript. If you require MyObject directly, you will end up in pain as you need to specify all the required properties, even though they're not actually required. If you can take any object, you've lost all type checking. If you use a generic parameter and require that the changes derive from T, you will get an error because the properties can't be optional, even though in this case we simply don't care.

So far the thing I have done is make the properties on MyObject be all optional, but this is really a poor solution as they may well actually be required, and you will end up in pain when you need to use them as such (such as assigning to a compatible interface but with required properties).

To fix this I would like to suggest an "optional" type modifier. The result is a type identical to the original, but all properties are optional. Ideally, this would not behave quite like the empty interface, and would be fully strict about not permitting extra properties.

For syntax I think that a simple ?, e.g. MyObject?, would be sufficient and unambiguous. There should be no emission or compatibility involved here. In terms of signature completion, etc, the "optional" type should be considered more-or-less identical to the original, so this should not require big changes for tooling.

The main use case here does not require support for subtyping, supertyping, or assignability relationships - the compiler can reject all of these. T? is only itself and nothing else. It would be nice if more could be supported but not necessary at all for this feature to be useful. In generic cases, a T? is about as far as it goes. There's also no need for the compiler to support any kind of T - T? compatibility, except that a T is obviously a valid T?. Otherwise these can be treated as two completely distinct types.

I think that this is possibly related to the Object.assign stuff, but my main intention here is that this is a very restricted feature that only needs to do a very small thing, so it should be much less problematic. Also, this is a type-level feature, not a runtime feature/expression, and you can't add extra properties with it.

@RyanCavanaugh
Copy link
Member

I believe this is the same as what's requested in #7004 ?

@RyanCavanaugh
Copy link
Member

Also #4889, #6613

@mhegazy mhegazy added the Duplicate An existing issue was already created label Sep 9, 2016
@markboyall
Copy link
Author

Nah, they're not the same. Although I want to do something ultimately similar, there's some details that are not the same. In #7004 the issuer wants to massively change how protected and private properties work w.r.t. type-checking. I've looked at #4889 and #6613, but they both involve supertypes that flat out don't work in this situation. I also narrowed the scope a lot.

@jaen
Copy link

jaen commented Sep 12, 2016

I think the contravariance proposal I suggested with #4889 would solve this (with variadic types though, if the merge has to be variadic; without that it would be simpler):

var merge: <Into, ...[From, ...Froms] super ...Into> => Into =
  (into, from, ...froms) => 
    from
      ? merge(/* merge operation for `into` and `from` here */, froms)
      : into;

interface MyObject {
  property1: number;
  property2: number;
}

obj: MyObject = ...;

obj = merge(obj, { property1: 1 }, { property2: 2 });

Since both { property1: number } and { property2: number } are supertypes of { property1: number, property2: number} it would typecheck.

@markboyall
Copy link
Author

markboyall commented Sep 12, 2016

We've tried applying generic constraints based on subtyping before. Unfortunately, that's totally broken because of optional properties. Consider

https://www.typescriptlang.org/play/#src=function%20merge%3CT%20extends%20U%2C%20U%3E(t%3A%20T%2C%20u%3A%20U)%20%7B%0D%0A%09return%3B%0D%0A%7D%0D%0A%0D%0Ainterface%20Original%20%7B%0D%0A%09property1%3F%3A%20number%3B%0D%0A%09property2%3A%20number%3B%0D%0A%7D%0D%0A%0D%0Alet%20original%3A%20Original%20%3D%20null%3B%0D%0Amerge(original%2C%20%7B%0D%0A%09property1%3A%201%0D%0A%7D)%3B%0D%0A

Because the original property is optional, and the literal is deduced as required, the supertyping relationship is broken.

Simply swapping the constraint around doesn't resolve this problem.

@jaen
Copy link

jaen commented Sep 12, 2016

Hmm, okay, as far as I understand what you're saying it is indeed problematic in this scenario.

I kind of assumed a required property would be covariant to an optional one (since it's a stronger constraint) and optional one contravariant to a required one (since it's a weaker constraint), because intuitively you can call a function with parameter of type {a?: number} with a parameter of type {a: number} and expect it to keep working and some functions safely admit a subtype of a type (ie. setState or merge).

Is there any concrete reason optionalness is invariant?

@markboyall
Copy link
Author

The problem is that the literal object's properties are always deduced as required. So in this case, it's the "base" that has a required property and the "derived" has an optional one, which is an invariant weakening.

Avoiding this kind of issue is exactly why I wanted to get straight to the point and just set all the properties to optional, as then there is no need to consider wider subtyping issues or constraints.

@mhegazy mhegazy closed this as completed Sep 20, 2016
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

4 participants