Describe the bug
The Exact<T> utility type that is included in generated TS code doesn't seem to do what it's intended to do. In fact it doesn't seem to do anything at all.
This example sums it up:
type Exact<T extends { [key: string]: unknown }> = {
[K in keyof T]: T[K];
};
type Foo = { x: number };
const notExactlyFoo = { x: 1, y: 1 };
// object literal with extra property is not assignable to Exact<Foo>
const a: Exact<Foo> = { x: 1, y: 1 }; // ts error
// object *variable* with extra property *is* assignable to Exact<Foo>
const b: Exact<Foo> = notExactlyFoo; // no ts error
// using just `Foo` instead of `Exact<Foo>` has the same effect
const c: Foo = { x: 1, y: 1 }; // tsc error
const d: Foo = notExactlyFoo; // no ts error
To Reproduce
CodeSandbox Link
Playground Link
Expected behavior
I would expect Exact<T> to be a T that has no extra properties. I made a mild assumption that this is what it's intended to do. Please correct me if I'm wrong!
Environment:
N/A
Additional context
This realization came from this thread on Reddit.
I noticed the same thing. Also, I don't believe it's really possible to have a true Exact type in TS yet. See this longstanding issue: https://github.com/microsoft/TypeScript/issues/12936
@dotansimha Would you accept a PR removing the Exact type?
Just a thought.. could the Exact type be used as a placeholder for when a true Exact type is possible?
Just a thought.. could the
Exacttype be used as a placeholder for when a trueExacttype _is_ possible?
I have my doubts a true Exact type will ever exist, but I guess another option would be to allow modifying the Exact type like we can do with Maybe. But as it stands now Exact doesn't do what it says it does and as far as I know it's not possible 馃槩.
But as it stands now Exact doesn't do what it says it does and as far as I know it's not possible 馃槩.
@scvnathan It's not possible right now but it might be possible in the future right? As far as not doing what it says it does, if we wanted to keep the type as a placeholder, the implementation could be stripped (i.e. type Exact<T> = T) and come with a doc comment to clarify the intention.
But probably the placeholder is not a great idea and it would be simpler to just remove Exact for now, and add it back if/when in the future it's supported in TS.
+1 for removing the Exact type
It's not possible right now but it might be possible in the future right?
Sure its possible, although https://github.com/microsoft/TypeScript/issues/12936 has been open since 2016 and there hasn't been any indication from the TS team that they want to tackle this. :(
As far as not doing what it says it does, if we wanted to keep the type as a placeholder, the implementation could be stripped (i.e. type Exact
= T) and come with a doc comment to clarify the intention.
But probably the placeholder is not a great idea and it would be simpler to just remove Exact for now, and add it back if/when in the future it's supported in TS.
Right, I think having a placeholder Exact would just continue to add confusion in addition to adding unnecessary noise to error messages. The funny thing is I don't think removing it would even be a breaking change.
I think having a placeholder Exact would just continue to add confusion in addition to adding unnecessary noise to error messages
Yeah, I have to agree.
The funny thing is I don't think removing it would even be a breaking change.
I think technically it would be a breaking change, since it has the potential to break things (the type is exported from the generated file, and could be used other places), but in practice I doubt many/any apps would be affected.
IMO it would be fine to release this change in a patch release.
@dotansimha What do you think about removing the Exact type in a patch release?
Sure its possible, although microsoft/TypeScript#12936 has been open since 2016 and there hasn't been any indication from the TS team that they want to tackle this.
@scvnathan Yeah, I guess it doesn't make sense to expect this any time soon, but I'm still hoping it will come someday, either in the form of a built-in Exact type, or in the form of some other feature(s) that can be used to implement our own Exact type.
@zenflow this might be a good candidate for Exact type:
type Exact<T, Shape> = T extends Shape ? Exclude<keyof T, keyof Shape> extends never ? T : never : never;
Example:
type Person = {name: string, age: number}
declare function savePerson<T>(person: Exact<T, Person>): void;
const tooFew = { name: 'David' };
const exact = { name: 'David', age: 41 }
const tooMany = { name: 'David', sex: 'male', age: 41 }
savePerson(tooFew); // doesn't work
savePerson(exact); // works well
savePerson(tooMany); // doesn't work
TBH I added Exact initially because I thought it will work better, and then I kept it for semantic reasons, to make sure developers know that they need to pass the exact structure (otherwise, it will fail on graphql-js validation for the variables).
I tend to keep it, just semantically, just because it might help someone to avoid runtime issues, and it doesn't effect anything else.
@slavikbez how can we apply that to codegen? I think we are not able to apply it on the Variables type, and we can do it only at the level of usage (like, with the auto-generated hooks), right?
Hi @dotansimha, you're right about auto-generated hooks. This is my understanding of how it can be applied:
...
export type GetClientsQuery = (
{ __typename?: 'Query' } &
...
);
...
export type QueryArgs = {
arg1: Scalars['String'];
arg2: Scalars['String'];
};
...
export const SomeQueryDocument = gql`
query SomeQuery($arg1: String!, $arg2: String!) {
someEntity(arg1: $arg1, arg2: $arg2) {
__typename
...
}
}
`
...
export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = defaultWrapper) {
return {
GetSomeEntity<T>(variables: Exact<T, QueryArgs>): Promise<SomeQuery> {
return withWrapper(() => client.request<SomeQuery>(print(SomeQueryDocument), variables));
}
}
};
Hi @dotansimha, you're right about auto-generated hooks. This is my understanding of how it can be applied:
... export type GetClientsQuery = ( { __typename?: 'Query' } & ... ); ... export type QueryArgs = { arg1: Scalars['String']; arg2: Scalars['String']; }; ... export const SomeQueryDocument = gql` query SomeQuery($arg1: String!, $arg2: String!) { someEntity(arg1: $arg1, arg2: $arg2) { __typename ... } } ` ... export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = defaultWrapper) { return { GetSomeEntity<T>(variables: Exact<T, QueryArgs>): Promise<SomeQuery> { return withWrapper(() => client.request<SomeQuery>(print(SomeQueryDocument), variables)); } } };
Awesome, this might require a major version for the plugins, since it will be a breaking change for some plugins. Any help here would be appreciated :)
Most helpful comment
@scvnathan It's not possible right now but it might be possible in the future right? As far as not doing what it says it does, if we wanted to keep the type as a placeholder, the implementation could be stripped (i.e.
type Exact<T> = T) and come with a doc comment to clarify the intention.But probably the placeholder is not a great idea and it would be simpler to just remove
Exactfor now, and add it back if/when in the future it's supported in TS.+1 for removing the
Exacttype