microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.19k stars 12.51k forks source link

Partial Types #11233

Closed RyanCavanaugh closed 8 years ago

RyanCavanaugh commented 8 years ago

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

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

mhegazy commented 8 years ago

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 commented 8 years ago

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

DanielRosenwasser commented 8 years ago

Do excess property checks still apply though?

RyanCavanaugh commented 8 years ago

Of course

tinganho commented 8 years ago

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

kitsonk commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago
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.

Igorbek commented 8 years ago

Otherwise regular structural subtype/assignability rules apply

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

RyanCavanaugh commented 8 years ago

Prototype (no generics, no sealedness) is working well

image

Open questions I have when implementing this:

kitsonk commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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

Maybe also consider partof.

kitsonk commented 8 years ago

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 commented 8 years ago

πŸ‘ partof might be the best alternative.

RyanCavanaugh commented 8 years ago

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

kitsonk commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

Questions to discuss today

RyanCavanaugh commented 8 years ago
yortus commented 8 years ago

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:

If they operate on sets 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 commented 8 years ago

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

yortus commented 8 years ago

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

RyanCavanaugh commented 8 years ago

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.

AlexGalays commented 8 years ago

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.

mhegazy commented 8 years ago

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?

yortus commented 8 years ago

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.

AlexGalays commented 8 years ago

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.

kitsonk commented 8 years ago

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

rawrmaan commented 8 years ago

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

RyanCavanaugh commented 8 years ago

I'm really torn. On this comment, vote

RyanCavanaugh commented 8 years ago

The people have spoken (and we decided to listen :wink: )

saschanaz commented 8 years ago

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

tinganho commented 8 years ago

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 8 years ago

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.

RyanCavanaugh commented 8 years ago

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.

rawrmaan commented 8 years ago

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.

alitaheri commented 8 years ago

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 8 years ago

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.

yortus commented 8 years ago

@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 8 years ago

Thanks @yortus that cleared it up for me!

wizzard0 commented 8 years ago

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

RyanCavanaugh commented 8 years ago

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 commented 8 years ago

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

rozzzly commented 8 years ago

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

RyanCavanaugh commented 8 years ago

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

AlexGalays commented 8 years ago

At last some good news in America!

saschanaz commented 8 years ago

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