microsoft / TypeScript

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

Partial Types (Optionalized Properties for Existing Types) #4889

Closed Gaelan closed 7 years ago

Gaelan commented 9 years ago
// Given:
interface Foo {
    simpleMember: number;
    optionalMember?: string;
    objectMember: X; // Where X is a inline object type, interface, or other object-like type 
}

// This:
var foo: partial Foo;
// Is equivalent to:
var foo: {simpleMember?: number, optionalMember?: string, objectMember?: X};

// And this:
var bar: deepPartial Foo;
// Is equivalent to:
var foo: {simpleMember?: number, optionalMember?: string, objectMember?: deepPartial X};

Potential Use Cases

danielfigueiredo commented 8 years ago

I would love to see this working, so many use cases for it. One is:

interface Test<A> {
  myMethod: <E super A>(arg: E) => E;
}

so that if I have:

interface Person extends Test<Person> {
  name: string;
  age: number;
}

I would like to be able to call

  person.myMethod({name: 'Luke'});
  person.myProp({age: 344});

without having to make the properties of Person optional

RyanCavanaugh commented 8 years ago

Some thoughts here as I think I'm going to try to get this into 2.1 as part of a push to improve typing of widely-used libraries.

First, I don't think super is the right concept for this. super should logically mean a recursive supertype, meaning this code would typecheck even though it doesn't really meet the intent of the use cases described so far (shallow optionalizing is what's desired, not deep):

interface Options {
  size: { width: number; height: number };
  color: string;
}
function foo<T super Options>(opts: T) { }
foo({ size: { width: 32 } }); // Forgot 'height'

So I think the next logical alternative is the "Make all properties optional" operator. I'll refer to that as partial T but am open to syntactic bikeshedding at a later date.

A big issue is that all-optional types (which is effectively what we're looking at synthesizing) behave very poorly in the type system. It's easy to have code like this:

interface Options {
  width?: number;
  size?: string;
}
function getOptions(): Options { return { width: 30, size: 'medium' } }
let x: Options = getOptions; // forgot to invoke, no error, lol wut

The "weak types" PR #3842 attempted to fix this by saying that all-optional properties (which I'll continue to refer to as "weak") can only be matched against types which have at least one declared property in common.

We could implement the "weak types" behavior for all types. I feel this is desirable, but it's maybe not sufficiently correct. Consider something like this:

interface MyState {
  width: number;
  size: string;
}
function setState(x: partial MyState) { /* ... */ }
let newOpts = { width: 10, length: 3 }; // Incorrect name 'length'
setState(newOpts); // OK but suspicious if not wrong

This gets remarkably worse any time you have a name?: string in the target type because now any Function is a plausible match!

What setState really wants, I believe, is an optional sealed type which doesn't accept additional properties. This should be a shallow sealing. We could introduce an additional type operator (ಠ_ಠ) sealed T which accepts any subtype of T which does not have additional declared (i.e. non-apparent in spec language) properties.

I haven't thought all the way through the implications of sealed as its own thing and am concerned it will suffer from the same "infectiousness" that non-null/readonly generally have (or be highly unsound) so that's maybe a last resort.

Alternatively, we could just say that any type produced by partial is implicitly sealed (and not provide the sealed type operator for the sake of reduced complexity). It's hard to imagine a case where you would want additional properties to show up in your target which weren't there in the first place.

Object.assign is much more complex because people use it to do both mixins/spreading and partial updates. Because the return value is unused (or at least ignorable), it's also hard to produce meaningful errors. Most likely we'll need to have Object.assign accept arbitrary types and return a spreaded type, and if you want to use it for partial updates you'll want to alias it through a const of a different, more specific type (e.g. const update: (x: T, ...extras: Array<partial T>) => T = Object.assign.bind(Object);)

fredgalvao commented 8 years ago

Great summary there, @RyanCavanaugh.

Although I like the sealed concept on it's own, and would maybe find value in it by itself (being provided, that is), I can agree with leaving it implicit and internal for now. Having partial be the new operator, and have it incur sealedness seems a great path, satisfying technical and syntactic needs for me.

zpdDG4gta8XKpMCd commented 8 years ago

@RyanCavanaugh why do we need yet another special case when there is an elegant generic solution? i mean higher kinded types of course (#1213), please consider

type MyDataProto<K<~>> = {
    one: K<number>;
    another: K<string>;
    yetAnother: K<boolean>;
}
type Identical<a> = a;
type Optional<a> = a?; // or should i say: a | undefined;
type GettableAndSettable<a> = { get(): a; set(value: a): void }

type MyData = MyDataProto<Identical>; // the basic type itself
type MyDataPartial = MyDataProto<Optional>; // "partial" type or whatever you call it
type MyDataGetterAndSetter = MyDataProto<GettableAndSettable>; // a proxy type over MyData
// ... etc
type MyDataYouNameItWhat = MyDataProto<SomeCrazyHigherKindedType>;
RyanCavanaugh commented 8 years ago

@aleksey-bykov that's quite nice but with state / setState in React you'd likely initialize the state with its default values, thus not producing any named type which could be made generic.

zpdDG4gta8XKpMCd commented 8 years ago

i am sorry i am not that familiar with React, can the problem be stated independently? an example?

kitsonk commented 8 years ago

We (@dojo) don't use React either, though we have a similar concept/problem. There are situations where there is a use case where you have a full type, where it is valid to express valid sub-types which have a direct relationship with the full type.

From the generic concept of state perspective, you have an object which will get mixed into the target object. Where the target object may have required properties, but the partial mixed in object would only express the properties that need to be mutated or changed. This also applies to the generic concept of patching objects server side (including the RESTful concept of PATCH) which expresses mutations to be done on an object, of which those mutations can be functionally expressed at a partial object.

For example, given the following object:

interface Person {
    firstName: string;
    lastName: string;
    amount: number;
}

const obj: Person = {
    firstName: 'Bob',
    lastName: 'Smith',
    amount: 20
}

I may functionally want to mutate/mixin/set state/etc. just the amount, but have it typed checked. Person is only valid though if it has all three properties. Currently the only way I can do that is by creating a separate "all optional" interface, which doesn't really properly express the right type, especially when there are covariant types:

interface PartialPerson {
    firstName?: string;
    lastName?: string;
    amount?: number;
}

function patch(target: Parent, value: PartialPerson): Parent {
    Object.assign(target, value);
}

patch(obj, { amount: 30 });

Especially if I make changes to Person it gets even uglier. To properly model the type that I want to pass in the argument, I want so express something that says it is any valid partial sub-type of Person. There is no concept of classical inheritance in these sub-types, which is why I agree super is not the right concept here.

@aleksey-bykov is that independent enough?

RyanCavanaugh commented 8 years ago

React's setState is basically identical to what @kitsonk just posted in terms of behavior.

zpdDG4gta8XKpMCd commented 8 years ago

it looks like there are a number of independent obstacles standing on the way to "partial" types:

  1. to get and easily maintain a sub-interface (let's call it a patch) of the original interface (as was shown HKT can do it)
  2. have some a way to prevent unrelated (compared to the original interface) properties from being a part of a patch (still a question)
  3. anything else?
kitsonk commented 8 years ago

@aleksey-bykov:

3) How to deal with tagged union types:

type State = { type: 'foo'; value: number; } | { type: 'bar'; value: string; };

const target: State = { type: 'foo', value: 100 };

function patch(value: partial State): void {
    Object.assign(target, value);
}

patch({ value: 'baz' }); // can't really type check this but breaks the type system
patch({ type: 'bar' }); // can't really type check this but breaks the type system

As far as point 2, I would say excess properties in literals would not match the partial type, though interface assignability between non-literals would exist (e.g. essentially what we have had since 1.6).

sandersn commented 8 years ago

Actually, have we defined what partial means with respect to unions? I think the naive thing to do would be to make the properties of partial T be the properties of T, adding undefined. That makes partial State useless as a tagged union: { type: 'foo' | 'bar' | undefined, value: number | string | undefined }, although it technically type checks.

You would notice the problem in a hurry if patch returned partial State, but a mutating function unfortunately swallows the type.

I don't see an easy solution to (3). We could start doing more complicated type algebra but so far we haven't. (For example, people would also like us to union call signatures on unions that have call signatures, but we don't.)

zpdDG4gta8XKpMCd commented 8 years ago

so to support 3 (discriminated unions) we need some properties to be optional and some to be still required, am i right? if so i got some good news, HKT can do it too:

type Kind = 'this' | 'that';
type MyDataProto<R<~>, O<~>> {
    kind: R<Kind>;
    one: O<string>;
    another: O<number>;
}
type Identical<a> = a;
type Optional<a> = a?; 
type MyData = MyDataProto<Identical, Identical>; // { kind: Kind; one: string; another: number }
type MyDataPatch = MyDataProto<Identical, Optional>; // { kind: Kind; one?: string; another?: number }

am i still missing something?

joewood commented 8 years ago

My vote is for partial and sealed to be defined independently, I can imagine a non-sealed partial to still be of use (e.g. mixins). The issue with tagged unions is trickier, but I would be happy to reduce the scope if we could get this in 2.1. I could imagine eventually that the "tagged" field in a tag-union would be a mandatory part of the partial type.

HKT looks interesting - and I guess that would work with setState - although not crazy for the syntax. The only problem here is that you would need to define upfront your type to be enabled to be treated as partial. The advantage of a type modifier is that it can be applied to any type.

AlexGalays commented 8 years ago

Just to be sure, would that new operator work with deep objects too? It would be quite limited otherwise.

Example:

deepUpdate(
  { a: { b: 33, c: 22, z: [1, 2] }, d: 'ok' },
  { a: { b: 44, z: [] } }
)

Also, yes the Optional operator must be usable dynamically, so that we can derive Optional types for any type (including parameterized types as is needed in that deepUpdate function definition)

RyanCavanaugh commented 8 years ago

@AlexGalays I think that's actually explicitly not what most people want (see above discussion on super vs partial). That isn't how Object.assign or React.setState work, for example.

AlexGalays commented 8 years ago

@RyanCavanaugh Any chance we can have both? e.g deepPartial

RyanCavanaugh commented 8 years ago

Anything is possible but let's discuss in a separate issue :smiley:

olmobrutall commented 8 years ago

I don't really get why sealed will be needed here. Extra members are already forbidden isn't it?

If I write:

var john: { name: string; age?: number } = { name: "John", age: 13, gender: "Male" };

I get

Error       Build:Type '{ name: string; age: number; gender: string; }' is not assignable to type '{ name: string; age?: number | undefined; }'.
RyanCavanaugh commented 8 years ago

@olmobrutall only when the object is "fresh". If there's a level of indirection where the type is manifested into a declaration, those errors no longer occur. Refer to this example from my previous longer comment:

interface MyState {
  width: number;
  size: string;
}
function setState(x: partial MyState) { /* ... */ }
let newOpts = { width: 10, length: 3 }; // Incorrect name 'length'
setState(newOpts); // OK but suspicious if not wrong
olmobrutall commented 8 years ago

Got it! This compiles

var temp = { name: "John", age: 13, gender: "Male" };
var john: { name: string; age?: number } = temp;

So TS uses the heuristic that if you are using a literal object you don't want inheritance stuff, preventing errors. I think this will work for 99% of the setState situations as well.

If this is a problem:

interface MyState {
  width: number;
  size: string;
}
function setState(x: partial MyState) { /* ... */ }
let newOpts = { width: 10, length: 3 }; // Incorrect name 'length'
setState(newOpts); // OK but suspicious if not wrong

then this is also a problem that we have today:

interface Person{
  name: string;
  age: number;
}

function savePerson(x: Person) { /* ... */ }
var temp = { name: "John", age: 13, gender: "Male" };
savePerson(temp)

In practice this error doesn't happen because you want to declare the type as soon as possible to get auto-complete for the members.

olmobrutall commented 8 years ago

Just to clarify, I'm not suggesting that partial should include sealed automatically, but to skip (or delay) sealed for now.

If in the future sealed becomes more of an issue we'll be in a good position to add it, with no breaking behavior.

joewood commented 8 years ago

Now that 2.0 is out, will this be added to the 2.1 roadmap? I don't see it there currently.

saschanaz commented 8 years ago

How will partial keyword play well with partial class(#563), although the latter does not exist on TS?

partial class A { // adding on class A
}
partial interface A { // not adding on interface A but makes properties optional
}
mDibyo commented 7 years ago

FYI: Looks like this has been resolved with #12114.

RyanCavanaugh commented 7 years ago

Indeed

cjbarth commented 7 years ago

How do mapped types fill the need of the various partial examples shown above?

dead-claudia commented 7 years ago

@cjbarth See here for some examples. It's pretty much a giant sledgehammer solving several constraint problems at once.

// Example from initial report
interface Foo {
    simpleMember: number;
    optionalMember?: string;
    objectMember: X; // Where X is a inline object type, interface, or other object-like type 
}

// This:
var foo: Partial<Foo>;
// Is equivalent to:
var foo: {simpleMember?: number, optionalMember?: string, objectMember?: X};

// Partial<T> in that PR is defined as this:
// Make all properties in T optional
interface Partial<T> {
    [P in keyof T]?: T[P];
}
mhegazy commented 7 years ago

Please note that Partial<T> is now part of the default library file.

mpseidel commented 7 years ago

You guys are all wizards to me. Hats off!

xogeny commented 7 years ago

@mhegazy When you say it is in the "default library file", does that mean I should be able to use it with TS 2.1.x? Do I need to import something? It doesn't seem to work.

DanielRosenwasser commented 7 years ago

@xogeny yes, keep an eye out for it in TypeScript 2.1 (or in our nightly builds).

xogeny commented 7 years ago

@DanielRosenwasser I'm still confused. As far as I can tell, I'm running TypeScript 2.1 and I don't see it. Are you saying this will come out in a patch release of 2.1?

mhegazy commented 7 years ago

typescript@2.1.1 is TS 2.1 RC. this does not have this change. TS 2.1 will be typescript@2.1.3 which has not shipped yet. you can use typescript@next today to get this feature working.

xogeny commented 7 years ago

Ah! OK, now I understand. I didn't realize these were release candidates. It would be much clearer if the version number reflected that, but I trust you had a good reason for doing it this way. I look forward to the final version!

kitsonk commented 7 years ago

From npm view typescript:

{ name: 'typescript',
  description: 'TypeScript is a language for application scale JavaScript development',
  'dist-tags': 
   { latest: '2.0.10',
     next: '2.2.0-dev.20161129',
     beta: '2.0.0',
     rc: '2.1.1',
     insiders: '2.0.6-insiders.20161017' }
}

From a release tag perspective on npm, it is labelled a RC and will not install automatically.

Also the commit tag in GitHub also refects the nature of the release: https://github.com/Microsoft/TypeScript/releases/tag/v2.1-rc.

I believe the challenge is that some of the wider tooling requires installable versions to have a proper npm semver number (e.g. #.#.#).

danielmeza commented 7 years ago

Another good approach is to auto generate files without break the user code, then we can auto generate code using transformation templates.