typescript-typedefs v0.4.0
Typescript-Typedefs
When using Apollo GrapQL you need to define the typeDefs for your schema. When using TypeScript you need to define interfaces to add proper typings. Because doing both of these things is rather a lot like code duplication, this package was created.
With this package you can simply define your model as a class, use the provided TypeScript decorators and call the generateTypeDefs
function, which will automatically generate your typeDefs for you.
Installation Guide
Install the packages
npm i typescript-typedefs
Update tsconfig
Since we use decorators, tsconfig must be configured to work with them.
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true
}
}
As well as some babel plugins.
"plugins": [
"babel-plugin-transform-typescript-metadata",
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose": true }]
]
Exports
This package exports
- Type - used as @Type() to decorate a class or @Type({implements: OtherClass})
- Input - used as @Input() to decorate a class
- Field - used as @Field(), @Field(String) or @Field({type: Int, nullable: true}) to decorate a class property
- Interface - used as @Interface() to decorate a class which should result in an interface in the typeDefs
- Int - used in an @Field() decorator
- Float - used in an @Field() decorator
- ID - used in an @Field() decorator
- generateTypeDefs - function used to create a typeDefs string from the array of decorated classes it recieves as argument.
Example
import { Type, Field, ID, Int, generateTypeDefs } from 'typescript-typedefs';
@Type()
class Course {
@Field()
name: string;
}
@Type()
class Student {
@Field(ID)
id: string;
@Field()
name: string;
@Field(String)
friendNames: string[];
@Field({ type: Int, nullable: true })
room: number;
@Field()
gpa: number;
@Field(Course)
courses: Course[];
}
@Interface()
class Book {
@Field()
author: string;
}
// option 1 for implementing
@Type()
class CourseBook extends Book {
@Field()
author: string;
@Field()
course: string;
}
// option 2 for implementing
@Type({ implements: Book })
class ColoringBook implements Book {
@Field()
author: string;
@Field()
designer: string;
}
const generatedTypeDefs = generateTypeDefs([Course, Student, Book, CourseBook, ColoringBook]);
results in a string:
`
interface Book {
author: String!
}
type Course {
name: String!
}
type Student {
id: ID!
name: String!
friendNames: [String!]!
room: Int
gpa: Float!
courses: [Course!]!
}
type CourseBook implements Book {
author: String!
course: String!
}
type ColoringBook implements Book {
author: String!
designer: String!
}
`;
which can then be used in makeExecutableSchema()
from Apollo server.
Directives
Directives can be used, either with or without params. The field directive
must always be set, as many params as desired can be added afterwards.
Use something like eslint-plugin-graphql to display your directives in your frontend.
import { Type, Field, ID, Int, generateTypeDefs } from 'typescript-typedefs';
import { merge } from 'lodash';
import { SchemaDirectiveVisitor } from "graphql-tools";
import { GraphQLField, GraphQLEnumValue } from 'graphql';
@Type()
class Course {
@Field()
name: string;
@Field({ directives: [{ directive: 'deprecated', reason: 'Use name instead' }] })
courseName: string;
@Field({ directives: [{ directive: 'deprecated' }] })
longCourseName: string;
}
const generatedTypeDefs = generateTypeDefs([Course]);
const courseTypeDefs = `
extend type Query {
courses: [Course!]!
}
${generatedTypeDefs}
`;
const typeDefs = gql`
directive @deprecated(reason: String = "No longer supported") on FIELD_DEFINITION | ENUM_VALUE
type Query {
_empty: string
}
type Mutation {
_empty: String
}
`;
class DeprecatedDirective extends SchemaDirectiveVisitor {
public visitFieldDefinition(field: GraphQLField<any, any>) {
field.isDeprecated = true;
field.deprecationReason = this.args.reason;
}
public visitEnumValue(value: GraphQLEnumValue) {
value.isDeprecated = true;
value.deprecationReason = this.args.reason;
}
}
const schema = makeExecutableSchema({
typeDefs: [typeDefs, courseTypeDefs],
resolver: merge(courseResolver),
schemaDirectives: {
deprecated: DeprecatedDirective,
},
});
FAQ
How to deal with circular dependencies
This packages makes use of reflect-metadata which has difficulties with circular references. To work around this, there are 2 options:
use forwardRef
// ./course/courseTypeDefs
@Type()
export class Course {
@Field(forwardRef(() => Student))
students: Student[];
}
// ./student/studentTypeDefs
@Type()
class Student {
@Field(Course)
courses: Course[];
}
export const studentCourseTypeDefs = generateTypeDefs([Student, Course]);
manually write type definition
// ./course/courseTypeDefs
@Type()
export class Course {
@Field(Student)
students: Student[]
}
// .student//studentTypeDefs
@Type()
class Student {
courses: Course[]
}
// ./schema
const generatedTypeDefs = generateTypeDefs([Course, Student])
const extendTypeDefs = gql`
extend type Student {
courses: [Course!]!
}
`
const schema = makeExecutableSchema({
typeDefs: [generatedTypeDefs, extendedTypeDefs],
resolvers: merge(...)
})
Why classes and not interfaces
Interfaces are not available at runtime, classes are.
Todo
- Add documentation to types/inputs
- Add enums
- Add nullable array elements
- Add nullables array elements:
Field({nullable: elementsAndArray})
- Add syntax for explicit arrays:
Field([String])
- investigate usage of 'string | undefined' instead of {nullable: true}
3 years ago
3 years ago
3 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago