microsoft/TypeScript

Partial Types

RyanCavanaugh opened this issue ยท 56 comments

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

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?

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

Do excess property checks still apply though?

Of course

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

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

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?

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.

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.

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

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

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.

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.

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

Maybe also consider partof.

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

๐Ÿ‘ partof might be the best alternative.

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

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. ๐Ÿ˜†

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.

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.

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
  • 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

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.

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

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)?

What I mean is this. union, intersection, subset and superset are all set theory terms that represent operations on set

This inconsistency (which domain does the operator apply in) was definitely identified as the worst thing about the name. As with many choices, sometimes you go with the least bad ๐Ÿ˜‰. The problem is that the "correct" name in the values-domain is superset, and superset T doesn't seem to accurately convey the intent of the behavior.

I don't think it's instructive to think too much about the names intersection and union as applied here. If we had used alphanumeric spellings, these would have been named T and U and T or U, respectively. But it's hard to write grammatically about or types and and types.

The interesting thing here is that this is the first operator which operates over the properties of a type. { x: number } | { y: string }, for example, doesn't produce a type that can be written without |. But subset does -- it's a mechanical transform over the properties of a type. It also isn't recursive the way a relationship like extends is. So applying the "we must use value-domain set theory naming" rule is perhaps being overconsistent, as subset doesn't actually perform any coherent set theory operation on the underlying type.

This is a very, very useful operator!

Are you leaving deep subsets out because it would be harder to implement?
A deep subset operator would be useful for libs that allow multiple deep property merges in one operation. It can be done with a series of shallow ones, but it's more verbose of course.

The name sounds fine to me.

A deep subset operator would be useful for libs that allow multiple deep property merges in one operation.

Do you have a specific library with this pattern in mind?

As with many choices, sometimes you go with the least bad ๐Ÿ˜‰

Fair enough, I just hope subset is indeed the least bad. As you say, "subset doesn't actually perform any coherent set theory operation on the underlying type." and "the "correct" name in the values-domain is superset". Now if in future TypeScript did want to add coherent subset/superset operators, the names subset and superset won't be available. IMHO that made other names for this operator (such as partial) less bad.

Do you have a specific library with this pattern in mind?

One of mine: https://github.com/AlexGalays/immupdate
Though it was written with JS in mind at the time and would have to be simplified a bit; I would trade some functionalities for type safety for sure.

@dojo has one too. In fact that is how we perform our state setting.

I was so excited about subset, I wasn't thinking about the deep nature of it or not. ๐Ÿ˜ญ

Just chiming in to say that I think partial is the clearest expression of the intended feature here.

I'm really torn. On this comment, vote

  • ๐ŸŽ‰ for subset
  • ๐Ÿ˜„ for partial
  • ๐Ÿ˜• for something else

The people have spoken (and we decided to listen ๐Ÿ˜‰ )

Too late but partof personally, as partial keyword is being used for partial class in some other languages...

I also cast my vote on partof, as partial is not consistent with the current design. All type operators has a noun + of. partial is an adjective and thus not fit as a name of a type operator. Though maybe more suitable as a modifier as in partial class.

zivni commented

What about using the question mark?

interface Foo{
   x: string
   y: number
}
//and then:
setState<T>(state: T?)

update<T>(src: T, update: T?)

var foo:Foo? = {x:5}

interface Bar extends Foo? {
   z: number
}

var bar:Bar = {z: 5} // O.K
var bar:Bar = {z: 5, y:6} // O.K
var bar:Bar = {y: 7} // Error

This is much needed. currently we mark all fields as optional, even if they are not, just to work around this. So if it is easier to implement this only for simple properties and not for everything - maybe you can start with it and then do the rest.

T? was originally slated to be a shorthand for either T | null or T | undefined or T | null | undefined, see #7426, so I don't think we would want to use it here either.

I've gotta say @saschanaz and @tinganho really helped change my mind on this one. I think partof is more consistent, especially since I saw the keysof proposal.

Almost all type operators share the same name schema: xxxof, typeof, instanceof, keysof, partial feels like a modifier like abstract static sealed etc. I would vote for partof too ๐Ÿ‘

any chance we can vote again?

rob3c commented

I think it's pretty clear from my extensive research on thesaurus.com that the most intuitive name is moiety! The organic chemistry reference should be obvious to all ;-)

Seriously, though, would someone mind confirming/correcting my understanding of the use of the term superset in this thread for characterizing the values of partial T with respect to T?

I get that partial T is the supertype of T, but I'll admit that I only thought of the subset of the properties of T when I first saw subset T above without thinking too much about the details of the supertype relationship. Is it because the values of T is defined as the set of all possible types consisting of optional/required combinations of T's properties, and T itself is only one of those combinations - i.e. the one where all properties are required? From that perspective, I can certainly see how partial T is a superset of T.

@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 commented

Thanks @yortus that cleared it up for me!

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

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.

@RyanCavanaugh any hints about what this new syntax will encompass / look like? ๐Ÿ˜ƒ

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

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

At last some good news in America!

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

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.

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.

@connor4312

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

this works just fine for me.

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

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

alvis commented

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