Closed Gaelan closed 7 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
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);
)
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 sealed
ness seems a great path, satisfying technical and syntactic needs for me.
@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>;
@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.
i am sorry i am not that familiar with React, can the problem be stated independently? an example?
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?
React's setState
is basically identical to what @kitsonk just posted in terms of behavior.
it looks like there are a number of independent obstacles standing on the way to "partial" types:
@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).
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.)
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?
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.
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)
@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.
@RyanCavanaugh Any chance we can have both? e.g deepPartial
Anything is possible but let's discuss in a separate issue :smiley:
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; }'.
@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
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.
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.
Now that 2.0 is out, will this be added to the 2.1 roadmap? I don't see it there currently.
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
}
FYI: Looks like this has been resolved with #12114.
Indeed
How do mapped types fill the need of the various partial
examples shown above?
@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];
}
Please note that Partial<T>
is now part of the default library file.
You guys are all wizards to me. Hats off!
@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.
@xogeny yes, keep an eye out for it in TypeScript 2.1 (or in our nightly builds).
@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?
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.
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!
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. #.#.#).
Another good approach is to auto generate files without break the user code, then we can auto generate code using transformation templates.
Potential Use Cases
deepPartial
)React.setState
(partial
)