This is a proposal for #4889 and a variety of other issues.
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
query
); this is a "shallow" operationpartial T
operatorA type partial T
behaves as follows:
T
, except that those properties are now optionalS
type is only assignable to type partial T
if S
has at least one property in common with T
partial (T | U)
is equivalent to (partial T) | (partial U)
partial (T & U)
does not expand (this would interact poorly with rule 2)T
is always a subtype of partial T
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 :+1:
// 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:
S
is a subtype of T
then subset S
is a subtype of subset T
Prototype (no generics, no sealedness) is working well
Open questions I have when implementing this:
subset T | U
equivalent to (subset T) | U
, or subset (T | U)
?subset (T | U)
is exactly (subset T) | (subset U)
, is this correct?subset (T & U)
is exactly (subset T) & (subset U)
, is this correct?subset
? Flow uses $Shape
which is not intuitive at allMy 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
, orsubset (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. :thinking:
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
subset T
when T
is a type parameter?undefined
to their domainssealed
/ final
laterT
and subset T
are assignable to subset T
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 T
) is well-named since the result type has a subset of propertiesT | U
) would be called intersection since the result type has the intersection of propertiesT & U
) would be called union since the result type has the union of propertiesIf they operate on sets of _values_:
subset T
) would be called superset since the result type is a superset of valuesT | U
) is well-named since the result type is the union of valuesT & U
) is well-named since the result type is the intersection of valuesUntil 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 :wink:. 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
subset
partial
The people have spoken (and we decided to listen :wink: )
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
.
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?
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.
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.
Also the related PR #17961. In fact, I'm facing the same problem. Will be extremely delighted when a solution becomes available.
Most helpful comment
I'm really torn. On this comment, vote
subset
partial