Is your feature request related to a problem? Please describe.
I'm using apollo.writeQuery to generate a relatively complicated local dataset.
Unfortunately due to the lack of a __typename in the generated type @ts-ignore has to be used in multiple places in order to access the __typename property.
This is a simple example but it illustrates the issue that is occurring without a __typename in the generated type:
import { User, Animal, CanMove } from "./types";
const cheetah: Animal = { id: "1", nickname: "Speedy", speed: 70 };
const bob: User = { id: "1", name: "Bob", speed: 15 };
function whatCanMove(canMove: CanMove): string {
// @ts-ignore
if (canMove.__typename === "User") {
return (canMove as User).name;
}
// @ts-ignore
if (canMove.__typename === "Animal") {
return (canMove as Animal).nickname;
}
return "Can't move";
}
console.log(whatCanMove(cheetah));
console.log(whatCanMove(bob));
Describe the solution you'd like
Add option to have __typename: string in generated interface types for example:
config:
withInterfaceTypename: true/false
Describe alternatives you've considered
Right now I'm type casting the interface type and using @ts-ignore to stop Typescript from failing for example:
// @ts-ignore
if (canMove.__typename === "User") {
return (canMove as User).name;
}
Additional context
Related issues:
You do have __typename there:
export type Animal = CanMove & {
__typename?: 'Animal',
id: Scalars['ID'],
nickname: Scalars['String'],
speed: Scalars['Int'],
};
It's an optional field, and you can force it with this configuration: https://graphql-code-generator.com/docs/generated-config/base-visitor#nonoptionaltypename-boolean-default-value-false
Also, what's the use case of using the base types as is? If you are generating client-side code, you should use types that are based on your selection set. In your example, you should use UserQuery type in your code, and not the base types.
I don't see a real need to creating objects that are of type Animal manually in client side.
If you are using writeQuery of Apollo, your code should be based on your query and it's selection set, and not on your schema types. Use fragments to make it simpler and have intermediate types.
Admittedly my use case for using writeQuery might be a bit narrow/overkill 😅.
I'm creating a static website for prototype purposes which has no backend backing it right now so the entire dataset is created locally and no queries/fragments have been written yet.
However, perhaps another more valid use case for having __typename: string in the base type/interface is for utility functions like the whatCanMove function, or for example a print/formatting function like:
function describeMovers(canMove: CanMove): string {
// Based on __typename print unique message
}
For anyone curious I ended up solving this by creating a union type for all the implementors of the interface something like:
schema
interface CanMove {
# ...
}
type User implements CanMove {
# ...
}
type Animal implements CanMove {
# ...
}
union Mover = Users | Animal
Typescript
function describeMovers(mover: Mover): string {
// ...
}
Maybe not ideal if you have hundreds of implementors...
I would also love to see __typename on the base interface as it would allow to process query results independent of the structure of the initial query.
PS: Thanks for your awesome work, @dotansimha!
I initially came to this issue thinking I needed __typename for interfaces.
My use case was that I was using the server typings with AppSync lambda datasources, and needed to do things like return heterogeneous types all implementing a single interface, but with branching logic on how each type was manipulated.
However, this issue (especially @dotansimha ) challenged me on this, and I realised that what I actually wanted was a union of types implementing each interface; all I was ever going to do with __typename on the interface was guard on a discriminated a union.
I saw some mentions (incarnated as possibleTypes?) for client typings, but not for the server-side. So, taking @ksrb 's example, I put this noddy plug-in together:
function plugin(schema /* , documents, config */) {
const implementingTypes = {};
const descriptions = {};
const typeMap = schema.getTypeMap();
Object.entries(typeMap).forEach(([name, type]) => {
if (type.description) descriptions[name] = type.description;
if (!type.getInterfaces) return;
const interfaces = type.getInterfaces();
interfaces.forEach(({ name: int }) => {
const implementing = implementingTypes[int] || [];
implementing.push(name);
implementingTypes[int] = implementing;
});
});
const unions = Object.entries(implementingTypes).map(([name, types]) => {
const plural = `${name}s`;
const declare = `export type ${plural} = ${types.join(' | ')};`;
const comment = `/** types implementing ${descriptions[name] || name} */`;
return [comment, declare].join('\n');
});
return unions.join('\n\n');
}
module.exports = { plugin };
People coming to this issue; is this useful? Do you want me to publish it? Do I even need to publish it- or have I missed something and the typescript plug-in already does this?
So I had more thinking about this issue, and it's been re-opened now.
This thing is - the typescript (and flow) plugins are "base" plugins - the types it generates are meant to be used by other plugins, so it could easily consume it.
If the consumer is typescript-operations, it will depend on the operation you are using. And in that matter, there is no chance that graphql will return the interface name as __typename, because fragment will make sure to resolve it to the actual type. Same with unions.
From the point of view of a GraphQL operation, there will always be __typename that points to a GraphQL type (and not interface or union).
Also, in this case, there is no reason for a developer to use the generated TypeScript interface type directly. Use the types generated for your operation. That means, that you don't need to use CanMove directly.
If you are using writeQuery or a similar method of Apollo, you are basically modifying a specific query or fragment in the store, which means that you need to use the types for that specific query.
If one of the fields in that query is a GraphQL interface, you'll be able to identify the actual type based on __typename, because the selection set represents all the possible output types that you can have.
Let's take for example the following schema:
type Query {
search(term: String!): [Node!]!
}
interface Node {
id: ID!
}
type User implements Node {
id: ID!
username: String!
email: String!
}
type Chat implements Node {
id: ID!
users: [User!]!
messages: [ChatMessage!]!
}
type ChatMessage implements Node {
id: ID!
content: String!
user: User!
}
And the following GraphQL operations:
query test {
search(term: "1") {
... on User {
id
username
}
... on Chat {
id
}
}
}
This will output the following for that operation when you are using typescript-operations:
export type TestQuery = (
{ __typename?: 'Query' }
& { search: Array<(
{ __typename?: 'User' }
& Pick<User, 'id' | 'username'>
) | (
{ __typename?: 'Chat' }
& Pick<Chat, 'id'>
) | { __typename?: 'ChatMessage' }> }
);
Now, if you want to check what type is search field, you should use it as the base, and not the type generated by typescript:
function doSomething(obj: TestQuery['search'][0]) {
if (obj.__typename === 'Chat') {
} else if (obj.__typename === 'ChatMessage') {
} else if (obj.__typename === 'User') {
}
}
But - if the consumer is typescript-resolvers, it might be a bit different, because in this case, you want to be able to use the TypeScript interface and have __typename pointing to the possible types (it works with unions now because we point to the all possible types, but it's not the case with GraphQL interfaces).
So we can add that option to add __typename for that purpose.
It's important to understand, that if you are using typescript-operations, there is not reason to have those. You are using the wrong types.
I initially came to this issue thinking I needed
__typenamefor interfaces.My use case was that I was using the server typings with AppSync lambda datasources, and needed to do things like return heterogeneous types all implementing a single interface, but with branching logic on how each type was manipulated.
However, this issue (especially @dotansimha ) challenged me on this, and I realised that what I actually wanted was a union of types implementing each interface; all I was ever going to do with
__typenameon the interface was guard on a discriminated a union.I saw some mentions (incarnated as possibleTypes?) for client typings, but not for the server-side. So, taking @ksrb 's example, I put this noddy plug-in together:
function plugin(schema /* , documents, config */) { const implementingTypes = {}; const descriptions = {}; const typeMap = schema.getTypeMap(); Object.entries(typeMap).forEach(([name, type]) => { if (type.description) descriptions[name] = type.description; if (!type.getInterfaces) return; const interfaces = type.getInterfaces(); interfaces.forEach(({ name: int }) => { const implementing = implementingTypes[int] || []; implementing.push(name); implementingTypes[int] = implementing; }); }); const unions = Object.entries(implementingTypes).map(([name, types]) => { const plural = `${name}s`; const declare = `export type ${plural} = ${types.join(' | ')};`; const comment = `/** types implementing ${descriptions[name] || name} */`; return [comment, declare].join('\n'); }); return unions.join('\n\n'); } module.exports = { plugin };People coming to this issue; is this useful? Do you want me to publish it? Do I even need to publish it- or have I missed something and the
typescriptplug-in already does this?
This is a good solution for my use case.
Although adding a __typename property with all possible type values to all interfaces would be preferable
I want to push this a little further.
The problem that @ksrb encountered is actually a product of the fact that interfaces in GraphQL are sealed (ie. they can only be implemented by a known, finite list of types) whereas interfaces in typescript are not (anyone can extend an interface wherever they like). GraphQL is designed with the expectation that the client will use this information when processing the response in order to handle each specific type in the response.
Essentially, a GraphQL interface is actually a _combination_ of a Typescript interface and Typescript union. We might represent it something like the following:
GraphQL
interface Bar {
x: String!
}
type BarA implements Bar {
a: String!
x: String!
}
type BarB implements Bar {
b: String!
x: String!
}
type Foo {
bar: Bar!
}
TypeScript
interface Bar {
x: string;
}
interface BarA extends Bar {
__typename?: "BarA";
a: string;
x: string;
}
interface BarB implements Bar {
__typename?: "BarB";
b: string;
x: string;
}
type BarUnion = BarA | BarB;
type Foo {
bar: BarUnion;
}
@mjeffryes You are right, and that's the exact output we have today. The generated TS type for GraphQL interface doesn't have __typename, and the implementing types does have.
I'm closing this one. It's been around for a long time, and I think we are not going to intentionally change that soon. We are always open for additional configuration flags and customizations, so if someone have a good use-case and want to pick this up and add this - feel free ;)
@dotansimha I’m not sure that @mjeffryes’ example is the output we have today. Taking his example, I get an output that is analogous to
type Foo {
bar: Bar
}
instead of
type Foo {
bar: BarUnion
}
Because of this, the __typename fields which are present on BarA and BarB, are missing in the value of bar inside Foo.
Would it be possible to explicitly include all implementations of an interface wherever an interface is used as a value? E.g.,
type Foo {
bar: BarA | BarB
}
This seems more correct, because the interface itself is typically missing several fields apart from __typename, so without these Foo would be incomplete.
(...)
This seems more correct, because the interface itself is typically missing several fields apart from
__typename, so without theseFoowould be incomplete.
Nevermind, as usual the problem was between the computer monitor and the chair — I should have used a union instead of an interface in the GraphQL schema.
Most helpful comment
So I had more thinking about this issue, and it's been re-opened now.
This thing is - the
typescript(andflow) plugins are "base" plugins - the types it generates are meant to be used by other plugins, so it could easily consume it.If the consumer is
typescript-operations, it will depend on the operation you are using. And in that matter, there is no chance thatgraphqlwill return the interface name as__typename, because fragment will make sure to resolve it to the actual type. Same with unions.From the point of view of a GraphQL operation, there will always be
__typenamethat points to a GraphQLtype(and notinterfaceorunion).Also, in this case, there is no reason for a developer to use the generated TypeScript
interfacetype directly. Use the types generated for your operation. That means, that you don't need to useCanMovedirectly.If you are using
writeQueryor a similar method of Apollo, you are basically modifying a specificqueryorfragmentin the store, which means that you need to use the types for that specific query.If one of the fields in that query is a GraphQL
interface, you'll be able to identify the actual type based on__typename, because the selection set represents all the possible output types that you can have.Let's take for example the following schema:
And the following GraphQL operations:
This will output the following for that operation when you are using
typescript-operations:Now, if you want to check what type is
searchfield, you should use it as the base, and not the type generated bytypescript:(You can try this here)
But - if the consumer is
typescript-resolvers, it might be a bit different, because in this case, you want to be able to use the TypeScriptinterfaceand have__typenamepointing to the possible types (it works with unions now because we point to the all possible types, but it's not the case with GraphQL interfaces).So we can add that option to add
__typenamefor that purpose.It's important to understand, that if you are using
typescript-operations, there is not reason to have those. You are using the wrong types.