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

Partial Types #11233

Closed
RyanCavanaugh opened this issue Sep 28, 2016 · 56 comments
Closed

Partial Types #11233

RyanCavanaugh opened this issue Sep 28, 2016 · 56 comments
Assignees
Labels
Committed The team has roadmapped this issue Fixed A PR has been merged for this issue Suggestion An idea for TypeScript

Comments

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Sep 28, 2016

This is a proposal for #4889 and a variety of other issues.

Use Cases

Many libraries, notably React, have a method which takes an object and updates corresponding fields on some other object on a per-key basis. It looks like this

function setState<T>(target: T, settings: ??T??) {
  // ...
}
let obj = { a: 2, b: "ok", c: [1] };
setState(obj, { a: 4 }); // Desired OK
setState(obj, { a: "nope"}); // Desired error
setState(obj, { b: "OK", c: [] }); // Desired OK
setState(obj, { a: 1, d: 100 }); // Desired error
setState(obj, window); // Desired error

Observations

  • The update is rarely recursive (an exception is Mongo's query); this is a "shallow" operation
  • It's not desirable to allow "new" properties to appear
  • We're not looking for a supertype of some type (due to the non-recursive nature)

Unary partial T operator

A type partial T behaves as follows:

  1. Its properties are the same as the properties of T, except that those properties are now optional
  2. A S type is only assignable to type partial T if S has at least one property in common with T
    • Otherwise regular structural subtype/assignability rules apply
  3. The type partial (T | U) is equivalent to (partial T) | (partial U)
  4. The type partial (T & U) does not expand (this would interact poorly with rule 2)
  5. Any type T is always a subtype of partial T
  6. The type partial T is equivalent to T if T is any, never, null, or undefined

More thoughts to come later, likely

@RyanCavanaugh RyanCavanaugh self-assigned this Sep 28, 2016
@mhegazy
Copy link
Contributor

mhegazy commented Sep 28, 2016

A S type is only assignable to type subset T if S has at least one property in common with T

I thought we are not lumping week type detection in this proposal. so why the change?

@RyanCavanaugh
Copy link
Member Author

Weak type detection everywhere would be a breaking change, whereas here we have an easy place to restrict the bad assignment.

@DanielRosenwasser
Copy link
Member

Do excess property checks still apply though?

@RyanCavanaugh
Copy link
Member Author

Of course

@DanielRosenwasser DanielRosenwasser added the Suggestion An idea for TypeScript label Sep 29, 2016
@tinganho
Copy link
Contributor

This is not the same as covariance/contravariance #1394 ?

@kitsonk
Copy link
Contributor

kitsonk commented Sep 29, 2016

It is one aspect of covariance I believe (in that subset T is covariant to T), but it is not a whole system of being able to deal with generic types in a full covariant/contravariant way. Covariance/contravariance implies the relationship of the whole type, versus just optionality of presence of particular properties of the type.

I would personally rather take this pragmatic approach to dealing with the usage patterns in JavaScript/TypeScript then wait for a total covariant/contravariant solution. I suspect this would not interfere with an eventual full solution (e.g. support C# type in T and out T generics).

@kitsonk
Copy link
Contributor

kitsonk commented Sep 29, 2016

On the weak type detection, I assume with something like this:

interface State {
    foo: string;
}

const a: subset State = {}; // not assignable?

The only challenge I see with that is that sometimes that might be unavoidable, but I guess that is an edge case.

Also what about something like this:

interface State {
    foo: string;
    bar: { baz: string; };
}

interface OptionalState {
    foo?: string;
}

interface LiteralState {
    foo: 'bar';
}

interface DeepState {
    bar: { baz: 'qat'; };
}

function setState<subset T>(state: T) { }

const a: OptionalState = {};
const b: LiteralState = { foo: 'bar' };
const c: DeepState = { bar: { baz: 'qat' } };

setState(a); // Ok?
setState(b); // Ok?
setState(c); // Ok?

@yortus
Copy link
Contributor

yortus commented Sep 29, 2016

Can you explain the choice of the name 'subset'?

type T = { a: string; b: number; }
type Subset = subset T; // equivalent to { a?: string; b?: number; }

The set of values in Subset is a superset of the set of values in T, if I understand this right?

Union and intersection types are named to describe how their value sets are constructed from the value sets of their constituent types. So it seems strange that subset T describes the construction of a value set that is a superset of T's values.

@RyanCavanaugh
Copy link
Member Author

const a: subset State = {}; // not assignable?

I forgot to mention the special case that the empty type is not subject to the "at least one property in common" rule 👍

// Corrected to what I think you meant -- 'subset' is not a legal type parameter prefix
function setState(state: subset State) { }
setState(a); // OK: OptionalState has 'foo' in common with 'State'
setState(b); // Error: type 'string' not assignable to { baz: string }
setState(c); // OK: { bar: { baz: 'qat' } } assignable to { bar: { baz: string } }

Can you explain the choice of the name 'subset'?

Hopefully the intuition is at least apparent - that subset T has a subset of the properties of T. Whether we name something according to the set theory operation applied to the properties or to the values is somewhat arbitrary and while consistency is desirable, I'm not sure there's a good name that conveys the intent when seen in the other formulation of the domain, especially since the rule isn't intended to be recursive (which rules out things like super IMHO). Open to bikeshedding on this point.

@Igorbek
Copy link
Contributor

Igorbek commented Sep 29, 2016

Otherwise regular structural subtype/assignability rules apply

that wouldn't work with generic types. Need to better describe assignability rules, like:

  • if type S is a subtype of T then subset S is a subtype of subset T

@RyanCavanaugh
Copy link
Member Author

RyanCavanaugh commented Oct 3, 2016

Prototype (no generics, no sealedness) is working well

image

Open questions I have when implementing this:

  • What happens to call and construct signatures?
  • What happens to index signatures?
  • What is the correct precedence in parsing, e.g. is subset T | U equivalent to (subset T) | U, or subset (T | U) ?
  • Initial thinking is that subset (T | U) is exactly (subset T) | (subset U), is this correct?
  • Initial thinking is that subset (T & U) is exactly (subset T) & (subset U), is this correct?
  • Better name than subset ? Flow uses $Shape which is not intuitive at all

@kitsonk
Copy link
Contributor

kitsonk commented Oct 4, 2016

My opinions for what they are worth:

  • What happens to call and construct signatures?

Wouldn't these be optional too. Actually that might come in super handy when trying to deal with creating decorated functions:

interface Foo {
    (): void;
    foo: string;
}

const foo: subset Foo = function foo() { };
foo.foo = 'bar';
default export <Foo> foo;

I guess though, that raises the question in my head, is a subtype "frozen" at the point it is evaluated and assigned? For example:

interface State {
    foo: string;
    bar: number;
}

let a: subset State = { foo: 'string' };
let b: subset State = { bar: 1 };

let c = a; // what gets inferred?
let a = b; // is this valid?
let b = c; // is this valid?
  • What happens to index signatures?

These should persist unmodified to the subset and essentially be the only thing "required" in the subset.

  • What is the correct precedence in parsing, e.g. is subset T | U equivalent to (subset T) | U, or subset (T | U) ?

Would it be the same order of precedence as the typeof keyword in the type position? Which then only operates on the next type. So typeof foo | Bar is always (typeof foo) | Bar and therefore subset T | U would always be (subset T) | U.

  • Initial thinking is that subset (T | U) is exactly (subset T) | (subset U), is this correct?
  • Initial thinking is that subset (T & U) is exactly (subset T) & (subset U), is this correct?

We heavily use type FooBarState = FooState & BarState and I can't think of a situation where that I would expect (subset FooState) & (subset BarState) wouldn't be equal to subset (FooState & BarState). I guess the one exception is how you deal with index properties. I am not familiar how they are dealt with in unions and intersections anyways.

@tinganho
Copy link
Contributor

tinganho commented Oct 4, 2016

I'm a little bit concerned that the keyword subset might not be appropriate in this case. My arguments are the same as @yortus, so I won't repeat them . IMO subset is not an improvement over partial, which was the original proposal.

Two "type" operator end with of, such as typeof and instanceof and not to mention a third proposed keysof.

Since @RyanCavanaugh mentioned that super is out of discussion, because it infers recursiveness. I tend to agree, so it rules out superset, supersetof, supertypeof etc.

What about shapeof? It's short, concise and consistent.

@yortus
Copy link
Contributor

yortus commented Oct 4, 2016

Agree with @tinganho that partial seems at least as good a name as subset. shapeof sounds ok too.

Maybe also consider partof.

@kitsonk
Copy link
Contributor

kitsonk commented Oct 4, 2016

What about shapeof? It's short, concise and consistent.

Well, but that sounds like it is extracting the structure, sort of like typeof and gives no indication that it is only part (or a subset) of the shape. I like subset but wouldn't disagree with something like partof or partialof.

I think the objection around partial is that it invokes the concept of partial classes which is a different thing all together (and might get added to TypeScript).

@tinganho
Copy link
Contributor

tinganho commented Oct 4, 2016

👍 partof might be the best alternative.

@RyanCavanaugh
Copy link
Member Author

I'm liking partial the more I think about it. 🤔

I'm also not 100% confident about what should actually be done about partial T as a target type. I don't think we actually want to do the property overlap test (it's too weird with call signatures / index signatures) which effectively means for any arbitrary T, partial T is an allowed target of any source (minus known incorrectly-typed declared properties).

@kitsonk
Copy link
Contributor

kitsonk commented Oct 4, 2016

I'm liking partial the more I think about it. 🤔

I have always liked it too, just assumed you shied away from it for good reason. 😆

@CreepGin
Copy link

CreepGin commented Oct 5, 2016

partial is in line with the original suggestion (#4889) from which this proposal was based on. People have been following this feature for a long time now (me included), any other keyword would really throw us off.

That said, if partial cannot work for one reason or another, I'd like to cast my vote on optional. Both words are adjectives and their ending ("al") suggests a change in quality of the type that follow. Other suggestions such as partof and shapeof, feel more like some kind of extraction.

@aluanhaddad
Copy link
Contributor

I don't like partial as much because it has the connotation that it will become or needs to become a full T in the future. It's really more of an arbitrary cross-section of T.
I think subset is the best I've heard so far.

@RyanCavanaugh
Copy link
Member Author

RyanCavanaugh commented Oct 10, 2016

Questions to discuss today

  • Should call/construct signatures stick around?
  • What about index signatures?
  • What is the behavior of these types when they are the target of a subtype/assignability relation?
    • Note: Carefully consider effects here when applying the distributive law to intersection types!
  • What is the behavior of subset T when T is a type parameter?
  • Bikeshed the name

@RyanCavanaugh
Copy link
Member Author

RyanCavanaugh commented Oct 10, 2016

  • Should call/construct signatures stick around?
    • No
  • What about index signatures?
    • Add undefined to their domains
  • What is the behavior of these types when they are the target of a subtype/assignability relation?
    • Keep existing behavior and consider sealed / final later
  • What is the behavior of subset T when T is a type parameter?
    • Only T and subset T are assignable to subset T
  • Bikeshed the name
    • subset remains the favorite

@yortus
Copy link
Contributor

yortus commented Oct 11, 2016

subset remains the favorite

Sounds like it's decided. I'm curious what the team and/or users think about the inconsistent basis this will introduce for describing TypeScript's type operators (including possible future ones)?

What I mean is this. union, intersection, subset and superset are all set theory terms that represent operations on sets. But what sets do they operate on in TypeScript?

If they operate on sets of properties:

  • subset (subset T) is well-named since the result type has a subset of properties
  • union (T | U) would be called intersection since the result type has the intersection of properties
  • intersection (T & U) would be called union since the result type has the union of properties

If they operate on sets of values:

  • subset (subset T) would be called superset since the result type is a superset of values
  • union (T | U) is well-named since the result type is the union of values
  • intersection (T & U) is well-named since the result type is the intersection of values

Until now all terminology consistently refered to sets of values (union, intersection, subtype=subset, supertype=superset). Now we have an operator that means the opposite (subset T = superset of values).

Just as union types were followed by intersection types, perhaps subset types will be followed by superset types. By this naming, superset T would represent a subset of values and be a subtype of T.

@tinganho
Copy link
Contributor

An another confusing thing is that subset includes set in the keyword. Which IMO indicates the set of values.

@yortus
Copy link
Contributor

yortus commented Oct 11, 2016

I can't quite get my head around it yet but I suspect subset T and superset T type operators could be precursors for supporting covariance/contravariance annotations (also mentioned by @tinganho and @kitsonk in earlier comments). @Igorbek do you have any thoughts about this? If they could be used in that way, would it make more sense for subset T to represent subtypes or supertypes of T (and vice versa for superset T)?

@yortus
Copy link
Contributor

yortus commented Nov 2, 2016

@rob3c I think your understanding is correct. Types can be thought of as a shorthand way of characterising a set of values. For example with type T = { foo: string }, T is the set of all values which are objects having a property called foo whose value is a string. So the value { foo: 'hi' } belongs to T, but { bar: 'baz' } and { foo: true } do not belong to T.

Now partial T represents all values that might have a foo. So clearly every value in T is also in partial T, but not vice-versa (e.g. an empty object is in partial T but not in T). By that logic partial T is a superset of T.

Having said all that, there is no law saying that terminology must refer to value sets. The bike-shedding in this thread is really about landing on a name that avoids confusion and is reasonably consistent with past and possible future namings.

@rob3c
Copy link

rob3c commented Nov 2, 2016

Thanks @yortus that cleared it up for me!

RyanCavanaugh added a commit to RyanCavanaugh/TypeScript that referenced this issue Nov 2, 2016
RyanCavanaugh added a commit to RyanCavanaugh/TypeScript that referenced this issue Nov 2, 2016
RyanCavanaugh added a commit to RyanCavanaugh/TypeScript that referenced this issue Nov 2, 2016
@wizzard0
Copy link

wizzard0 commented Nov 2, 2016

Chiming into the bikeshedding: what about fragment T / fragmentof T or incomplete T ?

@RyanCavanaugh RyanCavanaugh modified the milestones: Future, TypeScript 2.1.2 Nov 2, 2016
@RyanCavanaugh
Copy link
Member Author

We're tentatively pushing this out to the release after 2.1 -- looking at a general solution that would let partial T just be an interface in lib.d.ts using some fancy new syntax that would allow a large variety of other uses cases as well (at which point it would be a normal generic type Partial<T> and you could rename it yourself, as well as potentially have a DeepPartial<T>). Stay tuned.

@jkillian
Copy link

jkillian commented Nov 7, 2016

@RyanCavanaugh any hints about what this new syntax will encompass / look like? 😃

@rozzzly
Copy link

rozzzly commented Nov 7, 2016

@RyanCavanaugh will object spread stuffs still hit in 2.1 or do they have to come together?

@RyanCavanaugh
Copy link
Member Author

@jkillian see #12114 . It slices, it dices, it makes partial types!

@AlexGalays
Copy link

At last some good news in America!

@saschanaz
Copy link
Contributor

saschanaz commented Nov 9, 2016

@AlexGalays I hope you are talking about anything related to the partial types.

@RyanCavanaugh
Copy link
Member Author

We'll close this with the assumption that #12114 (which is merged now) addresses all these use cases adequately - chime in with a new issue if that turns out not to be the case.

@mhegazy mhegazy modified the milestones: TypeScript 2.1.3, Future Nov 14, 2016
@mhegazy mhegazy added the Fixed A PR has been merged for this issue label Nov 14, 2016
@connor4312
Copy link
Member

connor4312 commented Sep 29, 2017

Hi @RyanCavanaugh, mapped types are fantastic, but it doesn't seem to address the questions above with deep partial types / deep subsets. Mapped types and Partial<> allow for a shallow subset, but all nested objects must be complete (though there are some dirty solutions). A PartialDeep generic would be fantastic.

The use case is that there is an API I'm building against which allows for object merges of very complex/deep objects.

@Igorbek
Copy link
Contributor

Igorbek commented Sep 29, 2017

@connor4312

type DeepPartial<T> = {
    [P in keyof T]?: DeepPartial<T[P]>;
};

this works just fine for me.

@AlexGalays
Copy link

@Igorbek

This gives zero control over Arrays for instance. It will iterate over their keys and allow you to access their method as nullable ones (concat, reduce, etc) which makes no sense. Same with a Date, etc.

@Igorbek
Copy link
Contributor

Igorbek commented Sep 30, 2017

ok, understood. My use case did require it. You then might be interested in #6606 with its mapping capabilities.

@alvis
Copy link

alvis commented Oct 12, 2017

Also the related PR #17961. In fact, I'm facing the same problem. Will be extremely delighted when a solution becomes available.

@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
Committed The team has roadmapped this issue Fixed A PR has been merged for this issue Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests