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 (Optionalized Properties for Existing Types) #4889

Closed
Gaelan opened this issue Sep 20, 2015 · 86 comments
Closed

Partial Types (Optionalized Properties for Existing Types) #4889

Gaelan opened this issue Sep 20, 2015 · 86 comments
Assignees
Labels
Committed The team has roadmapped this issue Suggestion An idea for TypeScript

Comments

@Gaelan
Copy link

Gaelan commented Sep 20, 2015

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

  • Mongo Queries (deepPartial)
  • React.setState (partial)
@DanielRosenwasser DanielRosenwasser added the Suggestion An idea for TypeScript label Sep 21, 2015
@DanielRosenwasser
Copy link
Member

I brought this up recently with @mhegazy, and it can really come in handy, especially

  • When a constructor takes an options bag that has its own properties
interface Props { /*...*/ };

class Foo implements Props {
     constructor(opts: partial Props) { /*...*/}
}
  • When you want completion from members you're going to mix in but you don't want to implement all the required members.
let NewThing: NewMembers & (partial ThingImMixingInWith) = { /*completionHere*/

And I'm sure in other scenarios. How we choose to go about making this available is a separate matter.

@s-panferov
Copy link

Just for bookkeeping, it's the same as $Shape from #2710.

@Gaelan
Copy link
Author

Gaelan commented Sep 21, 2015

@s-panferov Does $Shape match deepPartial or partial? (deepPartial is recursive for sub-objects)

@kitsonk
Copy link
Contributor

kitsonk commented Sep 21, 2015

When you want completion from members you're going to mix in but you don't want to implement all the required members.

And type guard against anything that isn't actually in the interface.

I would suspect $Shape could match partial at the least if not deepPartial (e.g. a non-strict object literal check).

Why wouldn't the behaviour always just be like deepPartial... If you are already in the mode of not being strict, why would you ever suddenly not want that to be deep?

@Gaelan
Copy link
Author

Gaelan commented Sep 21, 2015

@kitsonk I was thinking of React.setState, or any other function that overwrites keys of one object with keys from another, not recursing.

@jbondc
Copy link
Contributor

jbondc commented Sep 22, 2015

This would work nicely:

type foo = {a?:string, b?:string}; 
type fooStrict = foo & any!optional; // {a: string, b: string}
type fooOptional = foo & any!strict; // {a?: string, b?: string}

@fredgalvao
Copy link

This would make typings for lodash/underscore pretty awesome.

Would also be usefull in cases where you get a partial model as a param on the constructor to build the actual model. This way we wouldn't need to create a sibling interface just to hold the same properties with different optionality.

class Person {
    name: string;
    surname: string;
    birthday: Date;
    numberOfThings: number;

    constructor(initialValues: partial Person) {
        this.name = initialValues.name;
        this.surname = initialValues.surname;
        this.birthday = initialValues.birthday;
        this.numberOfThings = initialValues.numberOfThings;
    }
}

new Person({name: 'Fred', surname: 'Galvão', numberOfThings: 2});

@dallonf
Copy link

dallonf commented Oct 12, 2015

Yes, this would be extremely handy! Another good use case is React "higher-order" components - I want to implement a component that acts just like another, "lower-order" component, except it automatically calculates the value of some of its props... example time:

interface IUserAvatarProps {
  url: string,
  size?: number
}

class UserAvatar extends React.Component<IUserAvatarProps, {}> {
  //...
}

interface ISmartUserAvatarProps implements partial IUserAvatarProps {
  userId: string
}

class SmartUserAvatar extends React.Component<ISmartUserAvatarProps, {avatarUrl: string}> {
  render() {
    return <UserAvatar url={this.state.avatarUrl} {...this.props} />;
  }

  componentWillMount() {
    // Fetch this.state.avatarUrl from this.props.userId
  }
}

// Later...
<SmartUserAvatar id="1234" size={32} />

@DomBlack
Copy link

Yet another use case would be backbone models which have defaults and the constructor takes a partial set of attributes which override the defaults:

interface CarData { make: string, model: string }

class Car extends Backbone.Model<CarData> {
  defaults: CarData = {
    make: 'BMW',
    model: '7'
  }
}

new Car({ model: 'X5' });

@jbrantly
Copy link

@DomBlack just pointed me to this. Thanks! Just for backward/forward reference, this was also discussed in #392.

@Strate
Copy link

Strate commented Nov 11, 2015

+1

@xogeny
Copy link

xogeny commented Nov 14, 2015

+1

For me, the big benefit of such functionality would be in being able to create typesafe APIs around the various libraries for immutable data structures like updeep or immutable.js. In both of these libraries, the efficiency of the library hinges on being able to modify data structures by passing in the "deltas" you wish to apply. And the fundamental issue with type safety is a way of expressing the types of these deltas in the context of a parametric type for the complete data.

Of course, some basic language level functionality for implementing or expressing lenses would also be a potential way of addressing the issue.

@mhegazy mhegazy added the Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. label Dec 9, 2015
@masbicudo
Copy link

There is a use case for the Object.assign (from ES6).

It could be defined using partial types like this:

function assign<T>(target: subset of T, ...sources: (subset of T)[]): T;

Now I could do this:

var x = Object.assign({name:"Miguel Angelo"}, {age:32});

As all arguments are stated to be subsets of T, the call expression type could be inferred to be:

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

That would be assignable to the following type:

interface IPerson {
  name: string;
  age?: number;
}

It'd be assignable because __anonymous is a super set of IPerson.

If this was possible, I could pass a variable y of type IPerson to the assign method. The following pattern is useful when changing an immutable object:

var x = Object.assign({}, y, {name:"Miguel Angelo"});

As y is not known to have age or not, the resulting inferred type would have an optional age, resulting in a type that is equal to the type IPerson itself. The inference process could see that, and infer that this call expression type really is IPerson.

The final case, would be when I state the type of T when calling. The inference algorithm could then make sure that the resulting type is what I expect, or give me an error:

var x = Object.assign<IPerson>({name:"Miguel"}, {age:32}); // the combined subsets is a valid IPerson
var x = Object.assign<IPerson>({}, {name: "Miguel"}); // the combined subsets is a valid IPerson
var x = Object.assign<ILawyer>(new Lawyer(), new Person()); // the combined subsets is a valid ILawyer
var x = Object.assign<IPerson>({}, {age: 30}); // ERROR: the combined subsets is a valid Person

For the keyword, it could be partial, subset of, or anything, but I propose it to be subset of, because then there could be a superset of. But then partial could have a complete counterpart... I don't know... anything is good enough.

Does this make sense?

@tomsdev
Copy link

tomsdev commented Jan 18, 2016

Sorry @masbicudo if I didn't understand your example correctly but I think it can already be typed using "type parameters as constraints", see the first post in #5949

@jcristovao
Copy link

I've done this as a test, and I am pretty excited with the possibilities this seems to open.

interface xpta 
  { a : number
  , b : string
  , c : number[]
  }

interface xptb
  { a : number
  , b : string
  }

function assertSubType<T extends U, U>(x: T, y: U):void { };

assertSubType(<xpta>null,{a: 6, b:'aa'}); /* compiles, does nothing as expected */
assertSubType(<xpta>null,{a: 6, d:4});     /* gives error in typescript */
assertSubType(<xpta>null,{a: 6, c:'aa'}); /* gives error in typescript */

Is it related?

@guncha
Copy link

guncha commented Feb 2, 2016

With the latest 1.8-beta, it's possible to get this to work when both type parameters are defined on the function. Unfortunately, I doesn't seem to solve the React.Component.setState use case as it is using generic type classes.

I.e.:

declare function setState<T, S extends T>(state: S, partial_state: T) // works!

interface Component<S> {
  state: S
  setState<T, S extends T>(partial_state: T) // doesn't work as S is considered a new type param
}

@hayeah
Copy link

hayeah commented Feb 11, 2016

I have the same issue as @guncha when trying to type a cursor structure

interface Cursor<T> {
  get(): T,
  set(data: T)

  merge<T extends U, U>(val: U)
}

+1

@mjohnsonengr
Copy link

Agree with @masbicudo about Object.assign() use case +1

@basarat
Copy link
Contributor

basarat commented Feb 25, 2016

@mjohnsonengr since TypeScript 1.8 Object.assign has a nice type definition 🌹

    /**
      * Copy the values of all of the enumerable own properties from one or more source objects to a 
      * target object. Returns the target object.
      * @param target The target object to copy to.
      * @param source The source object from which to copy properties.
      */
    assign<T, U>(target: T, source: U): T & U;

    /**
      * Copy the values of all of the enumerable own properties from one or more source objects to a 
      * target object. Returns the target object.
      * @param target The target object to copy to.
      * @param source1 The first source object from which to copy properties.
      * @param source2 The second source object from which to copy properties.
      */
    assign<T, U, V>(target: T, source1: U, source2: V): T & U & V;

    /**
      * Copy the values of all of the enumerable own properties from one or more source objects to a 
      * target object. Returns the target object.
      * @param target The target object to copy to.
      * @param source1 The first source object from which to copy properties.
      * @param source2 The second source object from which to copy properties.
      * @param source3 The third source object from which to copy properties.
      */
    assign<T, U, V, W>(target: T, source1: U, source2: V, source3: W): T & U & V & W;

    /**
      * Copy the values of all of the enumerable own properties from one or more source objects to a 
      * target object. Returns the target object.
      * @param target The target object to copy to.
      * @param sources One or more source objects from which to copy properties
      */
    assign(target: any, ...sources: any[]): any;

I might have read your question wrong. Just thought I'd try to be helpful still 🌹

@mjohnsonengr
Copy link

@basarat that works except I would still have to define each partial interface as in the below example. I suppose it's just a convenience factor, but it would be nice if I didn't have to define PersonName to get typing information on the name in the second arbitrary object. Having the second interface just seems like a nightmare to maintain.

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

interface PersonName {
    name: string;
}

var person = { name: "asdf", age: 100 };
Object.assign<Person, PersonName>({}, person, { name: "mjohnsonengr" })

I guess a possible shortcut is

var updatePersonName = (person: Person, name: string) => Object.assign({}, person, { name });

@basarat
Copy link
Contributor

basarat commented Feb 26, 2016

@mjohnsonengr I blindly copy pasted from lib.d.ts without actually looking at it. I should have linked to : https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#type-parameters-as-constraints 🌹

function assign<T extends U, U>(target: T, source: U): T {
    for (let id in source) {
        target[id] = source[id];
    }
    return target;
}

let x = { a: 1, b: 2, c: 3, d: 4 };
assign(x, { b: 10, d: 20 });
assign(x, { e: 0 });  // Error

@mjohnsonengr
Copy link

@basarat Ohh! Now that's fancy! I'll have to play with that. Thanks for pointing that out to me!!

@jcristovao
Copy link

One question, given this modified example from (my own example) above:

interface A 
  { a : number
  , b : string  }

interface O // optional additional field
  { a : number
  , o? : string  };

interface M // mandatory additional field
  { a: number
  , m: string };

function assertSubType<T extends U, U>(x: T, y: U):void { };

assertSubType(<A>null,<M>null); // gives error as expected
assertSubType(<A>null,<O>null); // does not give an error

So, my question is, is an object with an optional field not included on the 'extended object' a valid subtype?

@RyanCavanaugh
Copy link
Member

@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
Copy link

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
Copy link

olmobrutall commented Sep 20, 2016

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
Copy link

joewood commented Sep 23, 2016

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

@saschanaz
Copy link
Contributor

saschanaz commented Sep 27, 2016

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
Copy link

mDibyo commented Nov 17, 2016

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

@RyanCavanaugh RyanCavanaugh added Committed The team has roadmapped this issue and removed Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Nov 17, 2016
@RyanCavanaugh
Copy link
Member

Indeed

@cjbarth
Copy link

cjbarth commented Nov 17, 2016

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

@dead-claudia
Copy link

@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
Copy link
Contributor

mhegazy commented Nov 18, 2016

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

@mpseidel
Copy link

You guys are all wizards to me. Hats off!

@xogeny
Copy link

xogeny commented Nov 28, 2016

@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
Copy link
Member

DanielRosenwasser commented Nov 28, 2016

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

@xogeny
Copy link

xogeny commented Nov 28, 2016

@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
Copy link
Contributor

mhegazy commented Nov 28, 2016

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
Copy link

xogeny commented Nov 28, 2016

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
Copy link
Contributor

kitsonk commented Nov 29, 2016

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
Copy link

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

@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 Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests