Open blakeembrey opened 7 years ago
@jcalz The advanced type ExclusifyUnion isn't very safe:
const { ...fields } = o as AorB;
fields.field3.toUpperCase(); // it shouldn't be passed
The fields of fields
are all non-optional.
I don't think that has much to do with Exact types, but with what happens when you spread and then destructure a union-typed object . Any union will end up getting flattened out into a single intersection-like type, since it's pulling apart an object into individual properties and then rejoining them; any correlation or constraint between the constituents of each union will be lost. Not sure how to avoid it... if it's a bug, it might be a separate issue.
Obviously things will behave better if you do type guarding before the destructuring:
declare function isA(x: any): x is A;
declare function isB(x: any): x is B;
declare const o: AorB;
if (isA(o)) {
const { ...fields } = o;
fields.field3.toUpperCase(); // error
} else {
const { ...fields } = o;
fields.field3.toUpperCase(); // error
if (fields.field3) {
fields.field3.toUpperCase(); // okay
}
}
Not that this "fixes" the issue you see, but that's how I'd expect someone to act with a constrained union.
Maybe https://github.com/Microsoft/TypeScript/pull/24897 fixes the spread issue
i might be late to the party, but here is how you can at least make sure your types exactly match:
type AreSame<A, B> = A extends B
? B extends A ? true : false
: false;
const sureIsTrue: (fact: true) => void = () => {};
const sureIsFalse: (fact: false) => void = () => {};
declare const x: string;
declare const y: number;
declare const xAndYAreOfTheSameType: AreSame<typeof x, typeof y>;
sureIsFalse(xAndYAreOfTheSameType); // <-- no problem, as expected
sureIsTrue(xAndYAreOfTheSameType); // <-- problem, as expected
wish i could do this:
type Exact<A, B> = A extends B ? B extends A ? B : never : never;
declare function needExactA<X extends Exact<A, X>>(value: X): void;
Would the feature described in this issue help with a case where an empty/indexed interface matches object-like types, like functions or classes?
interface MyType
{
[propName: string]: any;
}
function test(value: MyType) {}
test({}); // OK
test(1); // Fails, OK!
test(''); // Fails, OK!
test(() => {}); // Does not fail, not OK!
test(console.log); // Does not fail, not OK!
test(console); // Does not fail, not OK!
Interface MyType
only defines an index signature and is used as the type of the only parameter of the function test
. Parameter passed to the function of type:
{}
, passes. Expected behavior.1
does not pass. Expected behavior (Argument of type '1' is not assignable to parameter of type 'MyType'.)''
does not pass. Expected behavior (`Argument of type '""' is not assignable to parameter of type 'MyType'.)() => {}
: Passes. Not expected behavior. Probably passes because functions are objects?console.log
Passes. Not expected behavior. Similar to arrow function.console
passes. Not expected behavior. Probably because classes are objects?The point is to only allow variables that exactly match the interface MyType
by being of that type already (and not implicitly converted to it). TypeScript seems to do a lot of implicit conversion based on signatures so this might be something that cannot be supported.
Apologies if this is off-topic. So far this issue is the closest match to the problem I explained above.
@Janne252 This proposal could help you indirectly. Assuming you tried the obvious Exact<{[key: string]: any}>
, here's why it would work:
{[key: string]: any}
.{[key: string]: any}
.{[key: string]: any}
.call
signature (it's not a string property).console
object passes because it's just that, an object (not a class). JS makes no separation between objects and key/value dictionaries, and TS is no different here apart from the added row-polymorphic typing. Also, TS doesn't support value-dependent types, and typeof
is simply sugar for adding a few extra parameters and/or type aliases - it's not nearly as magical as it looks.@blakeembrey @michalstocki @aleksey-bykov This is my way of doing exact types:
type Exact<A extends object> = A & {__kind: keyof A};
type Foo = Exact<{foo: number}>;
type FooGoo = Exact<{foo: number, goo: number}>;
const takeFoo = (foo: Foo): Foo => foo;
const foo = {foo: 1} as Foo;
const fooGoo = {foo: 1, goo: 2} as FooGoo;
takeFoo(foo)
takeFoo(fooGoo) // error "[ts]
//Argument of type 'Exact<{ foo: number; goo: number; }>' is not assignable to parameter of type 'Exact<{ //foo: number; }>'.
// Type 'Exact<{ foo: number; goo: number; }>' is not assignable to type '{ __kind: "foo"; }'.
// Types of property '__kind' are incompatible.
// Type '"foo" | "goo"' is not assignable to type '"foo"'.
// Type '"goo"' is not assignable to type '"foo"'."
const takeFooGoo = (fooGoo: FooGoo): FooGoo => fooGoo;
takeFooGoo(fooGoo);
takeFooGoo(foo); // error "[ts]
// Argument of type 'Exact<{ foo: number; }>' is not assignable to parameter of type 'Exact<{ foo: number; // goo: number; }>'.
// Type 'Exact<{ foo: number; }>' is not assignable to type '{ foo: number; goo: number; }'.
// Property 'goo' is missing in type 'Exact<{ foo: number; }>'.
It works for functions parameters, returns and even for assingments.
const foo: Foo = fooGoo;
// error
No runtime overhead. Only issue is that whenever you create new exact object you have to cast it against its type, but it's not a big deal really.
I believe the original example has the correct behavior: I expect interface
s to be open. In contrast, I expect type
s to be closed (and they are only closed sometimes). Here is an example of surprising behavior when writing a MappedOmit
type:
https://gist.github.com/donabrams/b849927f5a0160081db913e3d52cc7b3
The MappedOmit
type in the example only works as intended for discriminated unions. For non discriminated unions, Typescript 3.2 is passing when any intersection of the types in the union is passed.
The workarounds above using as TypeX
or as any
to cast have the side effect of hiding errors in construction!. We want our typechecker to help us catch errors in construction too! Additionally, there are several things we can generate statically from well defined types. Workarounds like the above (or the nominal type workarounds described here: https://gist.github.com/donabrams/74075e89d10db446005abe7b1e7d9481) stop those generators from working (though we can filter _
leading fields, it's a painful convention that's absolutely avoidable).
@aleksey-bykov fyi i think your implementation is 99% of the way there, this worked for me:
type AreSame<A, B> = A extends B
? B extends A ? true : false
: false;
type Exact<A, B> = AreSame<A, B> extends true ? B : never;
const value1 = {};
const value2 = {a:1};
// works
const exactValue1: Exact<{}, typeof value1> = value1;
const exactValue1WithTypeof: Exact<typeof value1, typeof value1> = value1;
// cannot assign {a:number} to never
const exactValue1Fail: Exact<{}, typeof value2> = value2;
const exactValue1FailWithTypeof: Exact<typeof value1, typeof value2> = value2;
// cannot assign {} to never
const exactValue2Fail: Exact<{a: number}, typeof value1> = value1;
const exactValue2FailWithTypeof: Exact<typeof value2, typeof value1> = value1;
// works
const exactValue2: Exact<{a: number}, typeof value2> = value2;
const exactValue2WithTypeof: Exact<typeof value2, typeof value2> = value2;
wow, please leave the flowers over here, presents go in that bin
One small improvement that can be made here:
By using the following definition of Exact
effectively creates a subtraction of B
from A
as A
& never
types on all of B
's unique keys, you can get more granular errors on the invalid properties:
type Omit<T, K> = Pick<T, Exclude<keyof T, keyof K>>;
type Exact<A, B = {}> = A & Record<keyof Omit<B, A>, never>;
Lastly, I wanted to be able to do this without having to add explicit template usage of the second B
template argument. I was able to make this work by wrapping with a method- not ideal since it affects runtime but it is useful if you really really need it:
function makeExactVerifyFn<T>() {
return <C>(x: C & Exact<T, C>): C => x;
}
interface Task {
title: string;
due?: Date;
}
const isOnlyTask = makeExactVerifyFn<Task>();
const validTask_1 = isOnlyTask({
title: 'Get milk',
due: new Date()
});
const validTask_2 = isOnlyTask({
title: 'Get milk'
});
const invalidTask_1 = isOnlyTask({
title: 5 // [ts] Type 'number' is not assignable to type 'string'.
});
const invalidTask_2 = isOnlyTask({
title: 'Get milk',
procrastinate: true // [ts] Type 'true' is not assignable to type 'never'.
});
@danielnmsft It seems weird to leave B
in Exact<A, B>
optional in your example, especially if it's required for proper validation. Otherwise, it looks pretty good to me. It looks better named Equal
, though.
@drabinowitz Your type Exact
does not actually represent what has been proposed here and probably should be renamed to something like AreExact
. I mean, you can't do this with your type:
function takesExactFoo<T extends Exact<Foo>>(foo: T) {}
However, your type is handy to implement the exact parameter type!
type AreSame<A, B> = A extends B
? B extends A ? true : false
: false;
type Exact<A, B> = AreSame<A, B> extends true ? B : never;
interface Foo {
bar: any
}
function takesExactFoo <T>(foo: T & Exact<Foo, T>) {
// ^ or `T extends Foo` to type-check `foo` inside the function
}
let foo = {bar: 123}
let foo2 = {bar: 123, baz: 123}
takesExactFoo(foo) // ok
takesExactFoo(foo2) // error
UPD1 This will not create +1 runtime function as in the solution of @danielnmsft and of course is much more flexible.
UPD2 I just realized that Daniel in fact made basically the same type Exact
as @drabinowitz did, but a more compact and probably better one. I also realized that I did the same thing as Daniel had done. But I'll leave my comment in case if someone finds it useful.
That definition of AreSame
/Exact
does not seem to work for union type.
Example: Exact<'a' | 'b', 'a' | 'b'>
results in never
.
This can apparently be fixed by defining type AreSame<A, B> = A|B extends A&B ? true : false;
@nerumo definitely found this for the same type of reducer function you showed.
Couple additional options from what you had:
typeof
. More useful if it's a very complicated type. To me when I look at this it's more explicitly obvious the intent is prevent extra properties.interface State {
name: string;
}
function nameReducer(state: State, action: Action<string>): typeof state {
return {
...state,
fullName: action.payload // THIS IS REPORTED AS AN ERROR
};
}
interface State {
name: string;
}
function nameReducer(state: State, action: Action<string>) {
return (state = {
...state,
fullName: action.payload // THIS IS REPORTED AS AN ERROR
});
}
typeof state
againinterface State {
name: string;
}
function nameReducer(state: State, action: Action<string>) {
const newState: typeof state = {
...state,
fullName: action.payload // THIS IS REPORTED AS AN ERROR
};
return newState;
}
...state
you can use Partial<typeof state>
for the type:interface State {
name: string;
}
function nameReducer(state: State, action: Action<string>) {
const newState: Partial<typeof state> = {
name: 'Simon',
fullName: action.payload // THIS IS REPORTED AS AN ERROR
};
return newState;
}
I do feel this whole conversation (and I just read the whole thread) missed the crux of the issue for most people and that is that to prevent errors all we want is a type assertion to prevent disallow a 'wider' type:
This is what people may try first, which doesn't disallow 'fullName':
return <State> {
...state,
fullName: action.payload // compiles ok :-(
};
This is because <Dog> cat
is you telling the compiler - yes I know what I'm doing, its a Dog
! You're not asking permission.
So what would be most useful to me is a stricter version of <Dog> cat
that would prevent extraneous properties:
return <strict State> {
...state,
fullName: action.payload // compiles ok :-(
};
The whole Exact<T>
type thing has many ripple through consequences (this is a long thread!). It reminds me of the whole 'checked exceptions' debate where it's something you think you want but it turns out it has many issues (like suddenly five minutes later wanting an Unexact<T>
).
On the other hand <strict T>
would act more like a barrier to prevent 'impossible' types getting 'through'. It's essentially a type filter that passes through the type (as has been done above with runtime functions).
However it would be easy for newcomers to assume it prevented 'bad data' getting through in cases where it would be impossible for it to do so.
So if I had to make a proposal syntax it would be this:
/// This syntax is ONLY permitted directly in front of an object declaration:
return <strict State> { ...state, foooooooooooooo: 'who' };
Back to the OP: in theory[1] with negated types you could write type Exact<T> = T & not Record<not keyof T, any>
. Then an Exact<{x: string}>
would forbid any types with keys other than x
from being assigned to it. Not sure if that's enough to satisfy what's being asked by everyone here, but it does seem to perfectly fit the OP.
[1] I say in theory because that's predicated on better index signatures as well
Curious to know if I have the issue described here. I have code like:
const Layers = {
foo: 'foo'
bar: 'bar'
baz: 'baz'
}
type Groups = {
[key in keyof Pick<Layers, 'foo' | 'bar'>]: number
}
const groups = {} as Groups
then it allows me to set unknown properties, which is what I don't want:
groups.foo = 1
groups.bar = 2
groups.anything = 2 // NO ERROR :(
Setting anything
still works, and key value type is any
. I was hoping it would be an error.
Is this what will be solved by this issue?
Turns out, I should have been doing
type Groups = {
[key in keyof Pick<typeof Layers, 'foo' | 'bar'>]: number
}
Note the added use of typeof
.
The Atom plugin atom-typescript
was trying hard not to fail, and eventually crashed. When I added typeof
, things went back to normal, and unknown props were no longer allowed which is what I was expecting.
In other words, when I was not using typeof
, atom-typescript
was trying to figure the type in other places of the code where I was using the objects of type Groups
, and it was allowing me to add unknown props and showing me a type hint of any
for them.
So I don't think I have the issue of this thread.
Another complication might be how to handle optional properties.
If you have a type that has optional properties what would Exact<T>
for those properties mean:
export type PlaceOrderResponse = {
status: 'success' | 'paymentFailed',
orderNumber: string
amountCharged?: number
};
Does Exact<T>
mean every optional property must be defined? What would you specify it as? Not 'undefined' or 'null' because that's has a runtime effect.
Does this now require a new way to specify a 'required optional parameter'?
For example what do we have to assign amountCharged
with in the following code sample to get it to satisfy the 'exactness' of the type? We're not being very 'exact' if we don't enforce this property to be at least 'acknowledged' somehow. Is it <never>
? It can't be undefined
or null
.
const exactOrderResponse: Exact<PlaceOrderResponse> =
{
status: 'paymentFailed',
orderNumber: '1001',
amountCharged: ????
};
So you may be thinking - it's still optional, and it is now exactly optional which just translates to optional. And certainly at runtime it would need to not be set, but it looks to me like we just 'broke' Exact<T>
by sticking in a question mark.
Maybe it is only when assigning a value between two types that this check needs to be made? (To enforce that they both include amountCharged?: number
)
Let's introduce a new type here for a dialog box's input data:
export type OrderDialogBoxData = {
status: 'success' | 'paymentFailed',
orderNumber: string
amountCharge?: number // note the typo here!
};
So let's try this out:
// run the API call and then assign it to a dialog box.
const serverResponse: Exact<PlaceOrderResponse> = await placeOrder();
const dialogBoxData: Exact<OrderDialogBoxData> = serverResponse; // SHOULD FAIL
I would expect this to fail of course because of the typo - even though this property is optional in both.
So then I came back to 'Why are we wanting this in the first place?'. I think it would be for these reasons (or a subset depending upon the situation):
If 'exact optional properties' aren't handled properly then some of these benefits are broken or greatly confused!
Also in the above example we've just 'shoehorned' Exact
in to try to avoid typos but only succeeded in making a huge mess! And it's now even more brittle than ever before.
I think what I often need isn't an actually an Exact<T>
type at all, it is one of these two :
NothingMoreThan<T>
or
NothingLessThan<T>
Where 'required optional' is now a thing. The first allows nothing extra to be defined by the RHS of the assignment, and the second makes sure everything (including optional properties) is specified on the RHS of an assignment.
NothingMoreThan
would be useful for payloads sent across the wire, or JSON.stringify()
and if you were to get an error because you had too many properties on RHS you'd have to write runtime code to select only the needed properties. And that's the right solution - because that's how Javascript works.
NothingLessThan
is kind of what we already have in typescript - for all normal assignments - except it would need to consider optional (optional?: number)
properties.
I don't expect these names to make any traction, but I think the concept is clearer and more granular than Exact<T>
...
Then, perhaps (if we really need it):
Exact<T> = NothingMoreThan<NothingLessThan<T>>;
or would it be:
Exact<T> = NothingLessThan<NothingMoreThan<T>>; // !!
This post is a result of a real problem I'm having today where I have a 'dialog box data type' that contains some optional properties and I want to make sure what's coming from the server is assignable to it.
Final note: NothingLessThan
/ NothingMoreThan
have a similar 'feel' to some of the comments above where type A is extended from type B, or B is extended from A. The limitation there is that they wouldn't address optional properties (at least I don't think they could today).
@simeyla You could just get away with the "nothing more than" variant.
for all T extends X: T
.for all T super X: T
A way to pick one or both explicitly would be sufficient. As a side effect, you could specify Java's T super C
as your proposed T extends NothingMoreThan<C>
. So I'm pretty convinced this is probably better than standard exact types.
I feel this should be syntax though. Maybe this?
extends T
- The union of all types assignable to T, i.e. equivalent to just plain T
.super T
- The union of all types T is assignable to.extends super T
, super extends T
- The union of all types equivalent to T. This just falls out of the grid, since only the type can be both assignable and assigned to itself.type Exact<T> = extends super T
- Sugar built-in for the common case above, to aid readability.This also makes it possible to implement #14094 in userland by just making each variant Exact<T>
, like Exact<{a: number}> | Exact<{b: number}>
.
I wonder if this also makes negated types possible in userland. I believe it does, but I'd need to do some complicated type arithmetic first to confirm that, and it's not exactly an obvious thing to prove.
I wonder if this also makes negated types possible in userland, since (super T) | (extends T) is equivalent to unknown. I believe it is, but I'd need to do some complicated type arithmetic first to confirm that, and it's not exactly an obvious thing to prove.
For (super T) | (extends T) === unknown
to hold assignability would need to be a total order.
@jack-williams Good catch and fixed (by removing the claim). I was wondering why things weren't working out initially when I was playing around a bit.
@jack-williams
"Nothing less than" is just normal types. TS does this implicitly, and every type is treated as equivalent
Yes and no. But mostly yes... ...but only if you're in strict
mode!
So I had a lot of situations where I needed a property to be logically 'optional' but I wanted the compiler to tell me if I had 'forgotten it' or misspelled it.
Well that's exactly what you get with lastName: string | undefined
whereas I had mostly got lastName?: string
, and of course without strict
mode you won't be warned of all the discrepancies.
I've always known about strict mode, and I can't for the life of me find a good reason why I didn't turn it on until yesterday - but now that I have (and I'm still wading through hundreds of fixes) it's much easier to get the behavior I wanted 'out of the box'.
I had been trying all kinds of things to get what I wanted - including playing with Required<A> extends Required<B>
, and trying to remove optional ?
property flags. That sent me down a whole different rabbit hole - (and this was all before I turned strict
mode on).
The point being that if you're trying to get something close to 'exact' types today then you need to start with enabling strict
mode (or whatever combination of flags gives the right checks). And if I needed to add middleName: string | undefined
later then boom - I'd suddenly find everywhere I needed to 'consider it' :-)
PS. thanks for your comments - was very helpful. I'm realizing I've seen A LOT of code that clearly isn't using strict
mode - and then people run into walls like I did. I wonder what can be done to encourage its use more?
@simeyla I think your feedback and thanks should be directed at @isiahmeadows!
I figured I'd write up my experiences with Exact types after implementing a basic prototype. My general thoughts are that the team were spot on with their assessment:
Our hopeful diagnosis is that this is, outside of the relatively few truly-closed APIs, an XY Problem solution.
I don't feel that the cost of introducing yet another object type is repaid by catching more errors, or by enabling new type relationships. Ultimately, exact types let me say more, but they didn't let me do more.
Examining some of the potential uses cases of exact types:
keys
and for ... in
.Having more precise types when enumerating keys seems appealing, but in practice I never found myself enumerating keys for things that were conceptually exact. If you precisely know the keys, why not just address them directly?
The assignability rule { ... } <: { ...; x?: T }
is unsound because the left type may include an incompatible x
property that was aliased away. When assigning from an exact type, this rule becomes sound. In practice I never use this rule; it seems more suited for legacy systems that would not have exact types to begin with.
I had pinned my last hope on exact types improving props passing, and simplification of spread types. The reality is that exact types are the antithesis of bounded polymorphism, and fundamentally non-compositional.
A bounded generic lets you specify props you care about, and pass the rest through. As soon as the bound becomes exact, you completely lose width subtyping and the generic becomes significantly less useful. Another problem is that one of the main tools of composition in TypeScript is intersection, but intersection types are incompatible with exact types. Any non-trivial intersection type with an exact component is going to be vacuous: exact types do not compose. For react and props you probably want row types and row polymorphism, but that is for another day.
Almost all the interesting bugs that might be solved by exact types are solved by excess property checking; The biggest problem is that excess property checking does not work for unions without a discriminant property; solve this and almost all of the interesting problems relevant for exact types go away, IMO.
@jack-williams I do agree it's not generally very useful to have exact types. The excess property checking concept is actually covered by my super T
operator proposal, just indirectly because the union of all types T is assignable to notably does not include proper subtypes of T.
I'm not heavily in support of this personally apart from maybe a T super U
*, since about the only use case I've ever encountered for excess property checking were dealing with broken servers, something you can usually work around by using a wrapper function to generate the requests manually and remove the excess garbage. Every other issue I've found reported in this thread so far could be resolved simply by using a simple discriminated union.
* This would basically be T extends super U
using my proposal - lower bounds are sometimes useful for constraining contravariant generic types, and workarounds usually end up introducing a lot of extra type boilerplate in my experience.
@isiahmeadows I certainly agree the lower bounded types can be useful, and if you can get exact types out of that, then that's a win for those that want to use them. I guess I should add a caveat to my post that is: I'm primarily addressing the concept of adding a new operator specifically for exact object types.
@jack-williams I think you missed my nuance that I was primarily referring to the exact types and related part of excess property checking. The bit about lower bounded types was a footnote for a reason - it was a digression that's only tangentially related.
I managed to write an implementation for this that will work for function arguments that require varying degrees of exactness:
// Checks that B is a subset of A (no extra properties)
type Subset<A extends {}, B extends {}> = {
[P in keyof B]: P extends keyof A ? (B[P] extends A[P] | undefined ? A[P] : never) : never;
}
// This can be used to implement partially strict typing e.g.:
// ('b?:' is where the behaviour differs with optional b)
type BaseOptions = { a: string, b: number }
// Checks there are no extra properties (Not More, Less fine)
const noMore = <T extends Subset<BaseOptions, T>>(options: T) => { }
noMore({ a: "hi", b: 4 }) //Fine
noMore({ a: 5, b: 4 }) //Error
noMore({ a: "o", b: "hello" }) //Error
noMore({ a: "o" }) //Fine
noMore({ b: 4 }) //Fine
noMore({ a: "o", b: 4, c: 5 }) //Error
// Checks there are not less properties (More fine, Not Less)
const noLess = <T extends Subset<T, BaseOptions>>(options: T) => { }
noLess({ a: "hi", b: 4 }) //Fine
noLess({ a: 5, b: 4 }) //Error
noLess({ a: "o", b: "hello" }) //Error
noLess({ a: "o" }) //Error |b?: Fine
noLess({ b: 4 }) //Error
noLess({ a: "o", b: 4, c: 5 }) //Fine
// We can use these together to get a fully strict type (Not More, Not Less)
type Strict<A extends {}, B extends {}> = Subset<A, B> & Subset<B, A>;
const strict = <T extends Strict<BaseOptions, T>>(options: T) => { }
strict({ a: "hi", b: 4 }) //Fine
strict({ a: 5, b: 4 }) //Error
strict({ a: "o", b: "hello" }) //Error
strict({ a: "o" }) //Error |b?: Fine
strict({ b: 4 }) //Error
strict({ a: "o", b: 4, c: 5 }) //Error
// Or a fully permissive type (More Fine, Less Fine)
type Permissive<A extends {}, B extends {}> = Subset<A, B> | Subset<B, A>;
const permissive = <T extends Permissive<BaseOptions, T>>(options: T) => { }
permissive({ a: "hi", b: 4 }) //Fine
permissive({ a: 5, b: 4 }) //Error
permissive({ a: "o", b: "hello" }) //Error
permissive({ a: "o" }) //Fine
permissive({ b: 4 }) //Fine
permissive({ a: "o", b: 4, c: 5 }) //Fine
https://www.npmjs.com/package/ts-strictargs https://github.com/Kotarski/ts-strictargs
I feel like I have a use case for this when wrapping React components, where I need to "pass through" props: https://github.com/Microsoft/TypeScript/issues/29883. @jack-williams Any thoughts on this?
@OliverJAsh Looks relevant, but I must admit I don't know React as well as most. I guess it would be helpful to work through how exact types can precisely help here.
type MyComponentProps = { foo: 1 };
declare const MyComponent: ComponentType<MyComponentProps>;
type MyWrapperComponent = MyComponentProps & { myWrapperProp: 1 };
const MyWrapperComponent: ComponentType<MyWrapperComponent> = props => (
<MyComponent
// We're passing too many props here, but no error!
{...props}
/>
);
Please correct me at any point I say something wrong.
I'm guessing the start would be to specify MyComponent
to accept an exact type?
declare const MyComponent: ComponentType<Exact<MyComponentProps>>;
In that case then we would get an error, but how do you fix the error? I'm assuming here that the wrapper components don't just have the same prop type all the way down, and at some point you really do need to dynamically extract a prop subset. Is this a reasonable assumption?
If MyWrapperComponent
props is also exact then I think it would be sufficient to do a destructuring bind. In the generic case this would require an Omit
type over an exact type, and I really don't know the semantics there. I'm guessing it could work like a homomorphic mapped type and retain the exact-ness, but I think this would require more thought.
If MyWrapperComponent
is not exact then it will require some run-time check to prove the exactness of the new type, which can only be done by explicitly selecting the properties you want (which doesn't scale as you say in your OP). I'm not sure how much you gain in this case.
Things that I haven't covered because I don't know how likely they are is the generic case, where props
is some generic type, and where you need to combine props like { ...props1, ...props2 }
. Is this common?
@Kotarski Did you publish it by any chance in NPM registry?
@gitowiec
@Kotarski Did you publish it by any chance in NPM registry?
https://www.npmjs.com/package/ts-strictargs https://github.com/Kotarski/ts-strictargs
I have this use-case:
type AB = { a: string, b: string }
type CD = { c: string, d: string }
type ABCD = AB & CD
// I want this to error, because the 'c' should mean it prevents either AB or ABCD from being satisfied.
const foo: AB | ABCD = { a, b, c };
// I presume that I would need to do this:
const foo: Exact<AB> | Exact<ABCD> = { a, b, c };
@ryami333 That does not need exact types; that just needs a fix to excess property checking: #13813.
@ryami333 If you are willing to use an extra type, I have a type that will do what you want it to, namely force a stricter version of unions :
type AB = { a: string, b: string }
type CD = { c: string, d: string }
type ABCD = AB & CD
type UnionKeys<T> = T extends any ? keyof T : never;
type StrictUnionHelper<T, TAll> = T extends any ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;
type StrictUnion<T> = StrictUnionHelper<T, T>
// Error now.
const foo: StrictUnion<AB | ABCD> = { a: "", b: "", c: "" };
@dragomirtitian Fascinating. It's curious to me why
type KeyofV1<T extends object> = keyof T
produces a different result than
type KeyofV2<T> = T extends object ? keyof T : never
Could someone explain this to me?
type AB = { a: string, b: string }
type CD = { c: string, d: string }
type ABCD = AB & CD
KeyofV1< AB | ABCD > // 'a' | 'b'
KeyofV2< AB | ABCD > // 'a' | 'b' | 'c' | 'e'
V1
gets the common keys of the union, V2
gets the keys of each union member and unions the result.
@weswigham Is there a reason they should be returning different results?
Yes? As I said - V1
gets the common keys to every union member, because the argument to keyof
ends up being keyof (AB | ABCD)
, which is just "A" | "B"
, while the version within the conditional only receives one union member at a time, thanks to the conditional distributing over its input, so it's essentially keyof AB | keyof ABCD
.
@weswigham So the conditional evaluates it more like this, like via some implicit loop?
type Union =
(AB extends object ? keyof AB : never) |
(ABCD extends object ? keyof ABCD : never)
When I'm reading that code, I'd normally expect the (AB | ABCD) extends object
check to operate as a single unit, checking that (AB | ABCD)
is assignable to object
, then it returning keyof (AB | ABCD)
as a unit, 'a' | 'b'
. The implicit mapping seems really strange to me.
@isiahmeadows You can look at distributive conditional types as a foreach for unions. They apply the conditional type to each member of the union in turn and the result is the union of each partial result.
So UnionKeys<A | B> = UnionKeys<A> | UnionKeys<B> =(keyof A) | (keyof B)
But only if the conditional type distributes, and it distributes only if the tested type is a naked type parameter. So:
type A<T> = T extends object ? keyof T : never // distributive
type B<T> = [T] extends [object] ? keyof T : never // non distributive the type parameter is not naked
type B<T> = object extends T ? keyof T : never // non distributive the type parameter is not the tested type
Thanks guys, I think I got it. I re-arranged it for my understanding; I believe that the NegativeUncommonKeys
is useful on its own as well. Here it is in case it is useful to someone else as well.
type UnionKeys<T> = T extends any ? keyof T : never;
type NegateUncommonKeys<T, TAll> = (
Partial<
Record<
Exclude<
UnionKeys<TAll>,
keyof T
>,
never
>
>
)
type StrictUnion<T, TAll = T> = T extends any
? T & NegateUncommonKeys<T, TAll>
: never;
I also understand why T
and TAll
are both there. The "loop effect", where T is tested and naked, means that each item in the union for T
is applied whereas the untested TAll
contains the original and complete union of all items.
This is the handbook segment on distributive conditional types.
@weswigham Yeah .. except I feel that section reads like it was written by one compiler engineer for another compiler engineer.
Conditional types in which the checked type is a naked type parameter are called distributive conditional types.
What are naked type parameters ? (and why don't they put some clothes on 😄)
i.e. T refers to the individual constituents after the conditional type is distributed over the union type)
Just yesterday I had a discussion about what this particular sentence means and why there was an emphasis on the word 'after'.
I think the documentation is written assuming prior knowledge and terminology that users might not always have.
The handbook section does make sense to me and it explains it much better, but I still am skeptical of the design choice there. It just doesn't logically make sense to me how that behavior would naturally follow from a set theoretic and type-theoretic perspective. It just comes across as a little too hackish.
naturally follow from a set theoretic and type-theoretic perspective
Take each item in a set and partition it according to a predicate.
That's a distributive operation!
Take each item in a set and partition it according to a predicate.
Although that only makes sense when you're talking about sets of sets (ie, a union type) which starts sounding an awful lot more like category theory.
This is a proposal to enable a syntax for exact types. A similar feature can be seen in Flow (https://flowtype.org/docs/objects.html#exact-object-types), but I would like to propose it as a feature used for type literals and not interfaces. The specific syntax I'd propose using is the pipe (which almost mirrors the Flow implementation, but it should surround the type statement), as it's familiar as the mathematical absolute syntax.
This syntax change would be a new feature and affect new definition files being written if used as a parameter or exposed type. This syntax could be combined with other more complex types.
Apologies in advance if this is a duplicate, I could not seem to find the right keywords to find any duplicates of this feature.
Edit: This post was updated to use the preferred syntax proposal mentioned at https://github.com/Microsoft/TypeScript/issues/12936#issuecomment-267272371, which encompasses using a simpler syntax with a generic type to enable usage in expressions.