Repository: apollographql/apollo-codegen
Pull request #304 (2017-11-25 22:05:05)

Add typescript-modern target
Move flow/typescript modern targets into a `javascript` folder to clean
things up.

Make minimal modifications to cp/pasted flow target to create valid
typescript code.

This is just a starting point -- as the babel 7 ts types mature, and we
start to get complete type definitions for babel-types w/ babel7, we
will need to adjust the nodes to use proper ts nodes.

lewisf: As I was working through this, I noticed some bugs in the flow-modern target so i fixed those up too!

ptal @martijnwalraven 

Patch:
diff --git a/src/cli.js b/src/cli.js
index 52f53c6e..48d44290 100755
--- a/src/cli.js
+++ b/src/cli.js
@@ -114,7 +114,7 @@ yargs
       target: {
         demand: false,
         describe: 'Code generation target language',
-        choices: ['swift', 'scala', 'json', 'ts', 'typescript', 'flow', 'flow-modern'],
+        choices: ['swift', 'scala', 'json', 'ts', 'ts-modern', 'typescript', 'typescript-modern', 'flow', 'flow-modern'],
         default: 'swift'
       },
       only: {
diff --git a/src/generate.ts b/src/generate.ts
index 99a26ff2..8a214c14 100644
--- a/src/generate.ts
+++ b/src/generate.ts
@@ -11,10 +11,13 @@ import { BasicGeneratedFile } from './utilities/CodeGenerator'
 import { generateSource as generateSwiftSource } from './swift';
 import { generateSource as generateTypescriptSource } from './typescript';
 import { generateSource as generateFlowSource } from './flow';
-import { generateSource as generateFlowModernSource } from './flow-modern';
+import { generateSource as generateFlowModernSource } from './javascript/flow';
+import { generateSource as generateTypescriptModernSource } from './javascript/typescript';
 import { generateSource as generateScalaSource } from './scala';
 
-type TargetType = 'json' | 'swift' | 'ts' | 'typescript' | 'flow' | 'scala' | 'flow-modern';
+type TargetType = 'json' | 'swift' | 'ts' | 'typescript'
+  | 'flow' | 'scala' | 'flow-modern' | 'typescript-modern'
+  | 'ts-modern';
 
 export default function generate(
   inputPaths: string[],
@@ -52,9 +55,11 @@ export default function generate(
       writeOperationIdsMap(context);
     }
   }
-  else if (target === 'flow-modern') {
+  else if (target === 'flow-modern' || target === 'typescript-modern' || target === 'ts-modern') {
     const context = compileToIR(schema, document, options);
-    const generatedFiles = generateFlowModernSource(context);
+    const generatedFiles = target === 'flow-modern'
+      ? generateFlowModernSource(context)
+      : generateTypescriptModernSource(context) ;
 
     // Group by output directory
     const filesByOutputDirectory: {
diff --git a/src/flow-modern/__tests__/helpers.ts b/src/javascript/flow/__tests__/helpers.ts
similarity index 100%
rename from src/flow-modern/__tests__/helpers.ts
rename to src/javascript/flow/__tests__/helpers.ts
diff --git a/src/flow-modern/codeGeneration.ts b/src/javascript/flow/codeGeneration.ts
similarity index 93%
rename from src/flow-modern/codeGeneration.ts
rename to src/javascript/flow/codeGeneration.ts
index 8e73fd43..f31c7a2a 100644
--- a/src/flow-modern/codeGeneration.ts
+++ b/src/javascript/flow/codeGeneration.ts
@@ -13,18 +13,18 @@ import {
   Fragment,
   SelectionSet,
   Field,
-} from '../compiler';
+} from '../../compiler';
 
 import {
   typeCaseForSelectionSet,
   Variant
-} from '../compiler/visitors/typeCase';
+} from '../../compiler/visitors/typeCase';
 
 import {
   collectAndMergeFields
-} from '../compiler/visitors/collectAndMergeFields';
+} from '../../compiler/visitors/collectAndMergeFields';
 
-import { BasicGeneratedFile } from '../utilities/CodeGenerator';
+import { BasicGeneratedFile } from '../../utilities/CodeGenerator';
 import FlowGenerator, { ObjectProperty, FlowCompilerOptions, } from './language';
 import Printer from './printer';
 
@@ -256,15 +256,9 @@ export class FlowAPIGenerator extends FlowGenerator {
 
       let res;
       if (field.selectionSet) {
-        const genericAnnotation = this.annotationFromScopeStack(this.scopeStack);
-        if (field.type instanceof GraphQLNonNull) {
-          genericAnnotation.id.name = genericAnnotation.id.name;
-        } else {
-          genericAnnotation.id.name = '?' + genericAnnotation.id.name;
-        }
-
+        const generatedTypeName = this.annotationFromScopeStack(this.scopeStack);
         res = this.handleFieldSelectionSetValue(
-          genericAnnotation,
+          generatedTypeName,
           field
         );
       } else {
@@ -279,9 +273,16 @@ export class FlowAPIGenerator extends FlowGenerator {
     });
   }
 
-  private handleFieldSelectionSetValue(genericAnnotation: t.GenericTypeAnnotation, field: Field) {
+  private handleFieldSelectionSetValue(
+    generatedTypeName: t.GenericTypeAnnotation,
+    field: Field,
+  ) {
     const { selectionSet } = field;
 
+    const selectionValueGeneratedTypeName = field.type instanceof GraphQLNonNull
+      ? generatedTypeName.id.name
+      : '?' + generatedTypeName.id.name;
+
     const typeCase = this.getTypeCasesForSelectionSet(selectionSet as SelectionSet);
     const variants = typeCase.exhaustiveVariants;
 
@@ -305,7 +306,7 @@ export class FlowAPIGenerator extends FlowGenerator {
 
       exportedTypeAlias = this.exportDeclaration(
         this.typeAliasObjectUnion(
-          genericAnnotation.id.name,
+          generatedTypeName.id.name,
           propertySets
         )
       );
@@ -316,7 +317,9 @@ export class FlowAPIGenerator extends FlowGenerator {
     return {
       name: field.alias ? field.alias : field.name,
       description: field.description,
-      annotation: genericAnnotation
+      annotation: t.genericTypeAnnotation(
+        t.identifier(selectionValueGeneratedTypeName)
+      )
     };
   }
 
diff --git a/src/flow-modern/helpers.ts b/src/javascript/flow/helpers.ts
similarity index 97%
rename from src/flow-modern/helpers.ts
rename to src/javascript/flow/helpers.ts
index 00da9583..69ae51b2 100644
--- a/src/flow-modern/helpers.ts
+++ b/src/javascript/flow/helpers.ts
@@ -12,7 +12,7 @@ import {
 
 import * as t from 'babel-types';
 
-import { CompilerOptions } from '../compiler';
+import { CompilerOptions } from '../../compiler';
 
 const builtInScalarMap = {
   [GraphQLString.name]: t.stringTypeAnnotation(),
diff --git a/src/flow-modern/index.ts b/src/javascript/flow/index.ts
similarity index 100%
rename from src/flow-modern/index.ts
rename to src/javascript/flow/index.ts
diff --git a/src/flow-modern/language.ts b/src/javascript/flow/language.ts
similarity index 85%
rename from src/flow-modern/language.ts
rename to src/javascript/flow/language.ts
index d2672073..369a6e8a 100644
--- a/src/flow-modern/language.ts
+++ b/src/javascript/flow/language.ts
@@ -5,7 +5,7 @@ import {
 
 import {
   CompilerOptions
-} from '../compiler';
+} from '../../compiler';
 
 import { createTypeAnnotationFromGraphQLTypeFunction } from './helpers';
 
@@ -70,7 +70,9 @@ export default class FlowGenerator {
         }
       });
 
-    const typeAlias = this.typeAliasObject(name, fields);
+    const typeAlias = this.typeAliasObject(name, fields, {
+      keyInheritsNullability: true
+    });
 
     typeAlias.leadingComments = [{
       type: 'CommentLine',
@@ -80,18 +82,19 @@ export default class FlowGenerator {
     return typeAlias;
   }
 
-  public objectTypeAnnotation(fields: ObjectProperty[], isInputObject: boolean = false) {
+  public objectTypeAnnotation(fields: ObjectProperty[], {
+    keyInheritsNullability = false
+  } : {
+    keyInheritsNullability?: boolean
+  } = {}) {
     const objectTypeAnnotation = t.objectTypeAnnotation(
       fields.map(({name, description, annotation}) => {
-        if (annotation.type === "NullableTypeAnnotation") {
-          t.identifier(name + '?')
-        }
 
         const objectTypeProperty = t.objectTypeProperty(
           t.identifier(
             // Nullable fields on input objects do not have to be defined
             // as well, so allow these fields to be "undefined"
-            (isInputObject && annotation.type === "NullableTypeAnnotation")
+            (keyInheritsNullability && annotation.type === "NullableTypeAnnotation")
               ? name + '?'
               : name
           ),
@@ -116,11 +119,17 @@ export default class FlowGenerator {
     return objectTypeAnnotation;
   }
 
-  public typeAliasObject(name: string, fields: ObjectProperty[]) {
+  public typeAliasObject(name: string, fields: ObjectProperty[], {
+    keyInheritsNullability = false
+  }: {
+    keyInheritsNullability?: boolean
+  } = {}) {
     return t.typeAlias(
       t.identifier(name),
       undefined,
-      this.objectTypeAnnotation(fields)
+      this.objectTypeAnnotation(fields, {
+        keyInheritsNullability
+      })
     );
   }
 
diff --git a/src/flow-modern/printer.ts b/src/javascript/flow/printer.ts
similarity index 100%
rename from src/flow-modern/printer.ts
rename to src/javascript/flow/printer.ts
diff --git a/src/flow-modern/types/augment-babel-types.ts b/src/javascript/types/augment-babel-types.ts
similarity index 59%
rename from src/flow-modern/types/augment-babel-types.ts
rename to src/javascript/types/augment-babel-types.ts
index f6cc11c0..796ff41d 100644
--- a/src/flow-modern/types/augment-babel-types.ts
+++ b/src/javascript/types/augment-babel-types.ts
@@ -11,4 +11,12 @@ declare module 'babel-types' {
   interface ObjectTypeAnnotation {
     exact: boolean
   }
+
+  type TSTypeAnnotation = {
+    typeAnnotaton: TSType
+  }
+
+  type TSType = {
+    // TODO: Complete this, or wait for babel-types in DefinitelyTyped to get updated.
+  }
 }
diff --git a/src/javascript/typescript/codeGeneration.ts b/src/javascript/typescript/codeGeneration.ts
new file mode 100644
index 00000000..3b3b26dc
--- /dev/null
+++ b/src/javascript/typescript/codeGeneration.ts
@@ -0,0 +1,361 @@
+import * as t from '@babel/types';
+import { stripIndent } from 'common-tags';
+import {
+  GraphQLEnumType,
+  GraphQLInputObjectType,
+  GraphQLNonNull,
+} from 'graphql';
+import * as path from 'path';
+
+import {
+  CompilerContext,
+  Operation,
+  Fragment,
+  SelectionSet,
+  Field,
+} from '../../compiler';
+
+import {
+  typeCaseForSelectionSet,
+  Variant
+} from '../../compiler/visitors/typeCase';
+
+import {
+  collectAndMergeFields
+} from '../../compiler/visitors/collectAndMergeFields';
+
+import { BasicGeneratedFile } from '../../utilities/CodeGenerator';
+import TypescriptGenerator, { ObjectProperty, TypescriptCompilerOptions, } from './language';
+import Printer from './printer';
+
+class TypescriptGeneratedFile implements BasicGeneratedFile {
+  fileContents: string;
+
+  constructor(fileContents: string) {
+    this.fileContents = fileContents;
+  }
+  get output() {
+    return this.fileContents
+  }
+}
+
+function printEnumsAndInputObjects(generator: TypescriptAPIGenerator, context: CompilerContext) {
+  generator.printer.enqueue(stripIndent`
+    //==============================================================
+    // START Enums and Input Objects
+    // All enums and input objects are included in every output file
+    // for now, but this will be changed soon.
+    // TODO: Link to issue to fix this.
+    //==============================================================
+  `);
+
+  context.typesUsed
+    .filter(type => (type instanceof GraphQLEnumType))
+    .forEach((enumType) => {
+      generator.typeAliasForEnumType(enumType as GraphQLEnumType);
+    });
+
+  context.typesUsed
+    .filter(type => type instanceof GraphQLInputObjectType)
+    .forEach((inputObjectType) => {
+      generator.typeAliasForInputObjectType(inputObjectType as GraphQLInputObjectType);
+    });
+
+  generator.printer.enqueue(stripIndent`
+    //==============================================================
+    // END Enums and Input Objects
+    //==============================================================
+  `)
+}
+
+export function generateSource(
+  context: CompilerContext,
+) {
+  const generator = new TypescriptAPIGenerator(context);
+  const generatedFiles: { [filePath: string]: TypescriptGeneratedFile } = {};
+
+  Object.values(context.operations)
+    .forEach((operation) => {
+      generator.fileHeader();
+      generator.typeAliasesForOperation(operation);
+      printEnumsAndInputObjects(generator, context);
+
+      const output = generator.printer.printAndClear();
+
+      const outputFilePath = path.join(
+        path.dirname(operation.filePath),
+        '__generated__',
+        `${operation.operationName}.js`
+      );
+
+      generatedFiles[outputFilePath] = new TypescriptGeneratedFile(output);
+    });
+
+  Object.values(context.fragments)
+    .forEach((fragment) => {
+      generator.fileHeader();
+      generator.typeAliasesForFragment(fragment);
+      printEnumsAndInputObjects(generator, context);
+
+      const output = generator.printer.printAndClear();
+
+      const outputFilePath = path.join(
+        path.dirname(fragment.filePath),
+        '__generated__',
+        `${fragment.fragmentName}.js`
+      );
+
+      generatedFiles[outputFilePath] = new TypescriptGeneratedFile(output);
+    });
+
+  return generatedFiles;
+}
+
+export class TypescriptAPIGenerator extends TypescriptGenerator {
+  context: CompilerContext
+  printer: Printer
+  scopeStack: string[]
+
+  constructor(context: CompilerContext) {
+    super(context.options as TypescriptCompilerOptions);
+
+    this.context = context;
+    this.printer = new Printer();
+    this.scopeStack = [];
+  }
+
+  fileHeader() {
+    this.printer.enqueue(
+      stripIndent`
+        /* tslint:disable */
+        // This file was automatically generated and should not be edited.
+      `
+    );
+  }
+
+  public typeAliasForEnumType(enumType: GraphQLEnumType) {
+    this.printer.enqueue(this.enumerationDeclaration(enumType));
+  }
+
+  public typeAliasForInputObjectType(inputObjectType: GraphQLInputObjectType) {
+    this.printer.enqueue(this.inputObjectDeclaration(inputObjectType));
+  }
+
+  public typeAliasesForOperation(operation: Operation) {
+    const {
+      operationType,
+      operationName,
+      selectionSet
+    } = operation;
+
+    this.scopeStackPush(operationName);
+
+    this.printer.enqueue(stripIndent`
+      // ====================================================
+      // GraphQL ${operationType} operation: ${operationName}
+      // ====================================================
+    `)
+
+    // The root operation only has one variant
+    // Do we need to get exhaustive variants anyway?
+    const variants = this.getVariantsForSelectionSet(selectionSet);
+
+    const variant = variants[0];
+    const properties = this.getPropertiesForVariant(variant);
+
+    const exportedTypeAlias = this.exportDeclaration(
+      this.typeAliasObject(operationName, properties)
+    );
+
+    this.printer.enqueue(exportedTypeAlias);
+    this.scopeStackPop();
+  }
+
+  public typeAliasesForFragment(fragment: Fragment) {
+    const {
+      fragmentName,
+      selectionSet
+    } = fragment;
+
+    this.scopeStackPush(fragmentName);
+
+    this.printer.enqueue(stripIndent`
+      // ====================================================
+      // GraphQL fragment: ${fragmentName}
+      // ====================================================
+    `);
+
+    const variants = this.getVariantsForSelectionSet(selectionSet);
+
+    if (variants.length === 1) {
+      const properties = this.getPropertiesForVariant(variants[0]);
+
+      const name = this.annotationFromScopeStack(this.scopeStack).id.name;
+      const exportedTypeAlias = this.exportDeclaration(
+        this.typeAliasObject(
+          name,
+          properties
+        )
+      );
+
+      this.printer.enqueue(exportedTypeAlias);
+    } else {
+      const unionMembers: t.FlowTypeAnnotation[] = [];
+      variants.forEach(variant => {
+        this.scopeStackPush(variant.possibleTypes[0].toString());
+        const properties = this.getPropertiesForVariant(variant);
+
+        const name = this.annotationFromScopeStack(this.scopeStack).id.name;
+        const exportedTypeAlias = this.exportDeclaration(
+          this.typeAliasObject(
+            name,
+            properties
+          )
+        );
+
+        this.printer.enqueue(exportedTypeAlias);
+
+        unionMembers.push(this.annotationFromScopeStack(this.scopeStack));
+
+        this.scopeStackPop();
+      });
+
+      this.printer.enqueue(
+        this.exportDeclaration(
+          this.typeAliasGenericUnion(
+            this.annotationFromScopeStack(this.scopeStack).id.name,
+            unionMembers
+          )
+        )
+      );
+    }
+
+    this.scopeStackPop();
+  }
+
+  private getVariantsForSelectionSet(selectionSet: SelectionSet) {
+    return this.getTypeCasesForSelectionSet(selectionSet).exhaustiveVariants;
+  }
+
+  private getTypeCasesForSelectionSet(selectionSet: SelectionSet) {
+    return typeCaseForSelectionSet(
+      selectionSet,
+      this.context.options.mergeInFieldsFromFragmentSpreads
+    );
+  }
+
+  private getPropertiesForVariant(variant: Variant): ObjectProperty[] {
+    const fields = collectAndMergeFields(
+      variant,
+      this.context.options.mergeInFieldsFromFragmentSpreads
+    );
+
+    return fields.map(field => {
+      const fieldName = field.alias !== undefined ? field.alias : field.name;
+      this.scopeStackPush(fieldName);
+
+      let res;
+      if (field.selectionSet) {
+        const genericAnnotation = this.annotationFromScopeStack(this.scopeStack);
+
+        res = this.handleFieldSelectionSetValue(
+          genericAnnotation,
+          field
+        );
+      } else {
+        res = this.handleFieldValue(
+          field,
+          variant
+        );
+      }
+
+      this.scopeStackPop();
+      return res;
+    });
+  }
+
+  private handleFieldSelectionSetValue(generatedTypeAnnotation: t.GenericTypeAnnotation, field: Field) {
+    const { selectionSet } = field;
+
+    const typeCase = this.getTypeCasesForSelectionSet(selectionSet as SelectionSet);
+    const variants = typeCase.exhaustiveVariants;
+
+    let exportedTypeAlias;
+    if (variants.length === 1) {
+      const variant = variants[0];
+      const properties = this.getPropertiesForVariant(variant);
+      exportedTypeAlias = this.exportDeclaration(
+        this.typeAliasObject(
+          this.annotationFromScopeStack(this.scopeStack).id.name,
+          properties
+        )
+      );
+    } else {
+      const propertySets = variants.map(variant => {
+        this.scopeStackPush(variant.possibleTypes[0].toString())
+        const properties = this.getPropertiesForVariant(variant);
+        this.scopeStackPop();
+        return properties;
+      })
+
+      exportedTypeAlias = this.exportDeclaration(
+        this.typeAliasObjectUnion(
+           generatedTypeAnnotation.id.name,
+          propertySets
+        )
+      );
+    }
+
+    this.printer.enqueue(exportedTypeAlias);
+
+
+    return {
+      name: field.alias ? field.alias : field.name,
+      description: field.description,
+      annotation: field.type instanceof GraphQLNonNull
+        ? generatedTypeAnnotation
+        : this.makeNullableAnnotation(generatedTypeAnnotation)
+    };
+  }
+
+  private handleFieldValue(field: Field, variant: Variant) {
+    let res;
+    if (field.name === '__typename') {
+      const annotations = variant.possibleTypes
+        .map(type => {
+          const annotation = t.stringLiteralTypeAnnotation();
+          annotation.value = type.toString();
+          return annotation;
+        });
+
+      res = {
+        name: field.alias ? field.alias : field.name,
+        description: field.description,
+        annotation: t.unionTypeAnnotation(annotations)
+      };
+    } else {
+      // TODO: Double check that this works
+      res = {
+        name: field.alias ? field.alias : field.name,
+        description: field.description,
+        annotation: this.typeAnnotationFromGraphQLType(field.type)
+      };
+    }
+
+    return res;
+  }
+
+  public get output(): string {
+    return this.printer.print();
+  }
+
+  scopeStackPush(name: string) {
+    this.scopeStack.push(name);
+  }
+
+  scopeStackPop() {
+    const popped = this.scopeStack.pop()
+    return popped;
+  }
+
+}
diff --git a/src/javascript/typescript/helpers.ts b/src/javascript/typescript/helpers.ts
new file mode 100644
index 00000000..69ae51b2
--- /dev/null
+++ b/src/javascript/typescript/helpers.ts
@@ -0,0 +1,78 @@
+import {
+  GraphQLBoolean,
+  GraphQLFloat,
+  GraphQLInt,
+  GraphQLID,
+  GraphQLList,
+  GraphQLNonNull,
+  GraphQLScalarType,
+  GraphQLString,
+  GraphQLType,
+} from 'graphql'
+
+import * as t from 'babel-types';
+
+import { CompilerOptions } from '../../compiler';
+
+const builtInScalarMap = {
+  [GraphQLString.name]: t.stringTypeAnnotation(),
+  [GraphQLInt.name]: t.numberTypeAnnotation(),
+  [GraphQLFloat.name]: t.numberTypeAnnotation(),
+  [GraphQLBoolean.name]: t.booleanTypeAnnotation(),
+  [GraphQLID.name]: t.stringTypeAnnotation(),
+}
+
+export function createTypeAnnotationFromGraphQLTypeFunction(
+  compilerOptions: CompilerOptions
+): Function {
+  return function typeAnnotationFromGraphQLType(type: GraphQLType, {
+    nullable
+  } = {
+    nullable: true
+  }): t.FlowTypeAnnotation {
+    if (type instanceof GraphQLNonNull) {
+      return typeAnnotationFromGraphQLType(
+        type.ofType,
+        { nullable: false }
+      );
+    }
+
+    if (type instanceof GraphQLList) {
+      const typeAnnotation = t.arrayTypeAnnotation(
+        typeAnnotationFromGraphQLType(type.ofType)
+      );
+
+      if (nullable) {
+        return t.nullableTypeAnnotation(typeAnnotation);
+      } else {
+        return typeAnnotation;
+      }
+    }
+
+    let typeAnnotation;
+    if (type instanceof GraphQLScalarType) {
+      const builtIn = builtInScalarMap[type.name]
+      if (builtIn) {
+        typeAnnotation = builtIn;
+      } else {
+        if (compilerOptions.passthroughCustomScalars) {
+          typeAnnotation = t.anyTypeAnnotation();
+        } else {
+          typeAnnotation = t.genericTypeAnnotation(
+            t.identifier(type.name)
+          );
+        }
+      }
+    } else {
+      typeAnnotation = t.genericTypeAnnotation(
+        t.identifier(type.name)
+      );
+    }
+
+    if (nullable) {
+      return t.nullableTypeAnnotation(typeAnnotation);
+    } else {
+      return typeAnnotation;
+    }
+  }
+}
diff --git a/src/javascript/typescript/index.ts b/src/javascript/typescript/index.ts
new file mode 100644
index 00000000..ce3df389
--- /dev/null
+++ b/src/javascript/typescript/index.ts
@@ -0,0 +1,1 @@
+export { generateSource } from './codeGeneration';
diff --git a/src/javascript/typescript/language.ts b/src/javascript/typescript/language.ts
new file mode 100644
index 00000000..013ea265
--- /dev/null
+++ b/src/javascript/typescript/language.ts
@@ -0,0 +1,175 @@
+import {
+  GraphQLEnumType,
+  GraphQLInputObjectType,
+} from 'graphql';
+
+import {
+  CompilerOptions
+} from '../../compiler';
+
+import { createTypeAnnotationFromGraphQLTypeFunction } from './helpers';
+
+import * as t from '@babel/types';
+
+export type ObjectProperty = {
+  name: string,
+  description?: string | null | undefined,
+  annotation: t.FlowTypeAnnotation
+}
+
+export interface TypescriptCompilerOptions extends CompilerOptions {
+  // Leaving this open for Typescript only compiler options
+}
+
+export default class TypescriptGenerator {
+  options: TypescriptCompilerOptions
+  typeAnnotationFromGraphQLType: Function
+
+  constructor(compilerOptions: TypescriptCompilerOptions) {
+    this.options = compilerOptions;
+
+    this.typeAnnotationFromGraphQLType = createTypeAnnotationFromGraphQLTypeFunction(compilerOptions);
+  }
+
+  public enumerationDeclaration(type: GraphQLEnumType) {
+    const { name, description } = type;
+    const enumMembers = type.getValues().map(({ value }) => {
+      // @ts-ignore
+      return t.tSEnumMember(
+        t.identifier(value),
+        t.stringLiteral(value)
+      );
+    });
+
+    const typeAlias = t.exportNamedDeclaration(
+      // @ts-ignore
+      t.tSEnumDeclaration(
+        t.identifier(name),
+        enumMembers
+      ),
+      []
+    );
+
+    typeAlias.leadingComments = [{
+      type: 'CommentLine',
+      value: ` ${description}`
+    } as t.CommentLine];
+
+    return typeAlias;
+  }
+
+  public inputObjectDeclaration(inputObjectType: GraphQLInputObjectType) {
+    const { name, description } = inputObjectType;
+
+    const fieldMap = inputObjectType.getFields();
+    const fields: ObjectProperty[] = Object.keys(inputObjectType.getFields())
+      .map((fieldName: string) => {
+        const field = fieldMap[fieldName];
+        return {
+          name: fieldName,
+          annotation: this.typeAnnotationFromGraphQLType(field.type)
+        }
+      });
+
+    const typeAlias = this.typeAliasObject(name, fields, {
+      keyInheritsNullability: true
+    });
+
+    typeAlias.leadingComments = [{
+      type: 'CommentLine',
+      value: ` ${description}`
+    } as t.CommentLine]
+
+    return typeAlias;
+  }
+
+  public objectTypeAnnotation(fields: ObjectProperty[], {
+    keyInheritsNullability = false
+  } : {
+    keyInheritsNullability?: boolean
+  } = {}) {
+    const objectTypeAnnotation = t.objectTypeAnnotation(
+      fields.map(({name, description, annotation}) => {
+        const objectTypeProperty = t.objectTypeProperty(
+          t.identifier(
+            // Nullable fields on input objects do not have to be defined
+            // as well, so allow these fields to be "undefined"
+            (keyInheritsNullability && annotation.type === "NullableTypeAnnotation")
+              ? name + '?'
+              : name
+          ),
+          annotation.type === "NullableTypeAnnotation"
+            ? this.makeNullableAnnotation(annotation.typeAnnotation)
+            : annotation
+        );
+
+        if (description) {
+          objectTypeProperty.trailingComments = [{
+            type: 'CommentLine',
+            value: ` ${description}`
+          } as t.CommentLine]
+        }
+
+        return objectTypeProperty;
+      })
+    );
+
+    return objectTypeAnnotation;
+  }
+
+  public typeAliasObject(name: string, fields: ObjectProperty[], {
+    keyInheritsNullability = false
+  }: {
+    keyInheritsNullability?: boolean
+  } = {}) {
+    return t.typeAlias(
+      t.identifier(name),
+      undefined,
+      this.objectTypeAnnotation(fields, {
+        keyInheritsNullability
+      })
+    );
+  }
+
+  public typeAliasObjectUnion(name: string, members: ObjectProperty[][]) {
+    return t.typeAlias(
+      t.identifier(name),
+      undefined,
+      t.unionTypeAnnotation(
+        members.map(member => {
+          return this.objectTypeAnnotation(member)
+        })
+      )
+    )
+  }
+
+  public typeAliasGenericUnion(name: string, members: t.FlowTypeAnnotation[]) {
+    return t.typeAlias(
+      t.identifier(name),
+      undefined,
+      t.unionTypeAnnotation(members)
+    );
+  }
+
+  public exportDeclaration(declaration: t.Declaration) {
+    return t.exportNamedDeclaration(declaration, []);
+  }
+
+  public annotationFromScopeStack(scope: string[]) {
+    return t.genericTypeAnnotation(
+      t.identifier(
+        scope.join('_')
+      )
+    );
+  }
+
+  public makeNullableAnnotation(annotation: t.FlowTypeAnnotation) {
+    return t.unionTypeAnnotation([
+      annotation,
+      // @ts-ignore
+      t.tSUndefinedKeyword(),
+      // @ts-ignore
+      t.tSNullKeyword()
+    ])
+  }
+}
diff --git a/src/javascript/typescript/printer.ts b/src/javascript/typescript/printer.ts
new file mode 100644
index 00000000..0da25775
--- /dev/null
+++ b/src/javascript/typescript/printer.ts
@@ -0,0 +1,114 @@
+import * as t from '@babel/types';
+import generate from '@babel/generator';
+
+import { stripIndent } from 'common-tags';
+
+type Printable = t.Node | string;
+
+export default class Printer {
+  private printQueue: Printable[] = []
+
+  public print(): string {
+    return this.printQueue
+      .reduce(
+        (document: string, printable) => {
+          if (typeof printable === 'string') {
+            return document + printable;
+          } else {
+            const documentPart = generate(printable).code;
+            return document + this.fixCommas(documentPart);
+          }
+        },
+        ''
+      ) as string;
+  }
+
+  public enqueue(printable: Printable) {
+    this.printQueue = [
+      ...this.printQueue,
+      '\n',
+      '\n',
+      printable
+    ];
+  }
+
+  public printAndClear() {
+    const output = this.print();
+    this.printQueue = [];
+    return output;
+  }
+
+  /**
+   * When using trailing commas on ObjectTypeProperties within
+   * ObjectTypeAnnotations, we get weird behavior:
+   * ```
+   * {
+   *   homePlanet: ?string // description
+   *   ,
+   *   friends: any  // description
+   * }
+   * ```
+   * when we want
+   * ```
+   * {
+   *   homePlanet: ?string, // description
+   *   friends: any         // description
+   * }
+   * ```
+   */
+  private fixCommas(documentPart: string) {
+    const lines = documentPart.split('\n');
+    let currentLine = 0;
+    let nextLine;
+    const newDocumentParts = [];
+    // Keep track of what column comments should start on
+    // to keep things aligned
+    let maxCommentColumn = 0;
+
+    while (currentLine !== lines.length) {
+      nextLine = currentLine + 1;
+      const strippedNextLine = stripIndent`${lines[nextLine]}`;
+      if (strippedNextLine.length === 1 && strippedNextLine[0] === ',') {
+        const currentLineContents = lines[currentLine];
+        const commentColumn = currentLineContents.indexOf('//');
+        if (maxCommentColumn < commentColumn) {
+          maxCommentColumn = commentColumn;
+        }
+
+        const [contents, comment] = currentLineContents.split('//');
+        newDocumentParts.push({
+          main: contents.replace(/\s+$/g, '') + ',',
+          comment: comment.trim()
+        });
+        currentLine++;
+      } else {
+        newDocumentParts.push({
+          main: lines[currentLine],
+          comment: null
+        });
+      }
+
+      currentLine++;
+    }
+
+    return newDocumentParts.reduce((memo: string[], part) => {
+      const {
+        main,
+        comment
+      } = part;
+
+      let line;
+      if (comment !== null) {
+        const spacesBetween = maxCommentColumn - main.length;
+        line = `${main}${' '.repeat(spacesBetween)} // ${comment}`
+      } else {
+        line = main;
+      }
+
+      return [
+        ...memo,
+        line
+      ];
+    }, []).join('\n');
+  }
+}


Test patch:
diff --git a/src/flow-modern/__tests__/__snapshots__/codeGeneration.ts.snap b/src/javascript/flow/__tests__/__snapshots__/codeGeneration.ts.snap
similarity index 98%
rename from src/flow-modern/__tests__/__snapshots__/codeGeneration.ts.snap
rename to src/javascript/flow/__tests__/__snapshots__/codeGeneration.ts.snap
index a22c642a..bd777257 100644
--- a/src/flow-modern/__tests__/__snapshots__/codeGeneration.ts.snap
+++ b/src/javascript/flow/__tests__/__snapshots__/codeGeneration.ts.snap
@@ -12,7 +12,7 @@ Object {
 // GraphQL query operation: HeroName
 // ====================================================
 
-export type ?HeroName_hero_Human_friends = {
+export type HeroName_hero_Human_friends = {
   __typename: \\"Human\\",
   name: string, // What this human calls themselves
 } | {
@@ -20,7 +20,7 @@ export type ?HeroName_hero_Human_friends = {
   id: string,   // The ID of the droid
 };
 
-export type ?HeroName_hero = {
+export type HeroName_hero = {
   __typename: \\"Human\\",
   name: string,                          // What this human calls themselves
   id: string,                            // The ID of the human
@@ -90,7 +90,7 @@ export type Episode = \\"NEWHOPE\\" | \\"EMPIRE\\" | \\"JEDI\\";
 // GraphQL fragment: humanFragment
 // ====================================================
 
-export type ?humanFragment_friends = {
+export type humanFragment_friends = {
   __typename: \\"Human\\",
   name: string, // What this human calls themselves
 } | {
@@ -308,7 +308,7 @@ export type HeroName_hero_Human_friends = {
   name: string, // The name of the character
 };
 
-export type ?HeroName_hero = {
+export type HeroName_hero = {
   __typename: \\"Human\\",
   name: string,                          // What this human calls themselves
   id: string,                            // The ID of the human
@@ -364,7 +364,7 @@ export type HeroName_hero_Droid_friends = {
   id: string, // The ID of the character
 };
 
-export type ?HeroName_hero = {
+export type HeroName_hero = {
   __typename: \\"Human\\",
   name: string,                          // What this human calls themselves
   id: string,                            // The ID of the human
@@ -435,8 +435,8 @@ export type Episode = \\"NEWHOPE\\" | \\"EMPIRE\\" | \\"JEDI\\";
 // The input object sent when someone is creating a new review
 type ReviewInput = {
   stars: number,
-  commentary: ?string,
-  favorite_color: ?ColorInput,
+  commentary?: ?string,
+  favorite_color?: ?ColorInput,
 };
 
 // The input object sent when passing in a color
@@ -488,8 +488,8 @@ export type Episode = \\"NEWHOPE\\" | \\"EMPIRE\\" | \\"JEDI\\";
 // The input object sent when someone is creating a new review
 type ReviewInput = {
   stars: number,
-  commentary: ?string,
-  favorite_color: ?ColorInput,
+  commentary?: ?string,
+  favorite_color?: ?ColorInput,
 };
 
 // The input object sent when passing in a color
@@ -541,8 +541,8 @@ export type Episode = \\"NEWHOPE\\" | \\"EMPIRE\\" | \\"JEDI\\";
 // The input object sent when someone is creating a new review
 type ReviewInput = {
   stars: number,
-  commentary: ?string,
-  favorite_color: ?ColorInput,
+  commentary?: ?string,
+  favorite_color?: ?ColorInput,
 };
 
 // The input object sent when passing in a color
@@ -589,8 +589,8 @@ export type Episode = \\"NEWHOPE\\" | \\"EMPIRE\\" | \\"JEDI\\";
 // The input object sent when someone is creating a new review
 type ReviewInput = {
   stars: number,
-  commentary: ?string,
-  favorite_color: ?ColorInput,
+  commentary?: ?string,
+  favorite_color?: ?ColorInput,
 };
 
 // The input object sent when passing in a color
@@ -779,8 +779,8 @@ export type Episode = \\"NEWHOPE\\" | \\"EMPIRE\\" | \\"JEDI\\";
 // The input object sent when someone is creating a new review
 type ReviewInput = {
   stars: number,
-  commentary: ?string,
-  favorite_color: ?ColorInput,
+  commentary?: ?string,
+  favorite_color?: ?ColorInput,
 };
 
 // The input object sent when passing in a color
diff --git a/src/flow-modern/__tests__/codeGeneration.ts b/src/javascript/flow/__tests__/codeGeneration.ts
similarity index 97%
rename from src/flow-modern/__tests__/codeGeneration.ts
rename to src/javascript/flow/__tests__/codeGeneration.ts
index 2c796015..2d5a3069 100644
--- a/src/flow-modern/__tests__/codeGeneration.ts
+++ b/src/javascript/flow/__tests__/codeGeneration.ts
@@ -1,13 +1,13 @@
 import { parse } from 'graphql';
 
-import { loadSchema } from '../../loading';
-const schema = loadSchema(require.resolve('../../../test/fixtures/starwars/schema.json'));
+import { loadSchema } from '../../../loading';
+const schema = loadSchema(require.resolve('../../../../test/fixtures/starwars/schema.json'));
 
 import {
   compileToIR,
   CompilerOptions,
   CompilerContext,
-} from '../../compiler';
+} from '../../../compiler';
 
 import { generateSource } from '../codeGeneration';
 
diff --git a/src/flow-modern/types/babel7.ts b/src/javascript/types/babel7.ts
similarity index 100%
rename from src/flow-modern/types/babel7.ts
rename to src/javascript/types/babel7.ts
diff --git a/src/javascript/typescript/__tests__/__snapshots__/codeGeneration.ts.snap b/src/javascript/typescript/__tests__/__snapshots__/codeGeneration.ts.snap
new file mode 100644
index 00000000..8a3e24a7
--- /dev/null
+++ b/src/javascript/typescript/__tests__/__snapshots__/codeGeneration.ts.snap
@@ -0,0 +1,862 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Typescript codeGeneration fragment spreads with inline fragments 1`] = `
+Object {
+  "__generated__/HeroName.js": TypescriptGeneratedFile {
+    "fileContents": "
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL query operation: HeroName
+// ====================================================
+
+export type HeroName_hero_Human_friends = {
+  __typename: \\"Human\\",
+  name: string, // What this human calls themselves
+} | {
+  __typename: \\"Droid\\",
+  id: string,   // The ID of the droid
+};
+
+export type HeroName_hero = {
+  __typename: \\"Human\\",
+  name: string,                                            // What this human calls themselves
+  id: string,                                              // The ID of the human
+  homePlanet: string | undefined | null,                   // The home planet of the human, or null if unknown
+  friends: HeroName_hero_Human_friends | undefined | null, // This human's friends, or an empty list if they have none
+} | {
+  __typename: \\"Droid\\",
+  name: string,                                            // What others call this droid
+  id: string,                                              // The ID of the droid
+  appearsIn: (?Episode)[],                                 // The movies this droid appears in
+};
+
+export type HeroName = {
+  hero: HeroName_hero | undefined | null
+};
+
+//==============================================================
+// START Enums and Input Objects
+// All enums and input objects are included in every output file
+// for now, but this will be changed soon.
+// TODO: Link to issue to fix this.
+//==============================================================
+
+// The episodes in the Star Wars trilogy
+export enum Episode {
+  NEWHOPE = \\"NEWHOPE\\",
+  EMPIRE = \\"EMPIRE\\",
+  JEDI = \\"JEDI\\",
+}
+
+//==============================================================
+// END Enums and Input Objects
+//==============================================================",
+  },
+  "__generated__/droidFragment.js": TypescriptGeneratedFile {
+    "fileContents": "
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL fragment: droidFragment
+// ====================================================
+
+export type droidFragment = {
+  __typename: \\"Droid\\",
+  appearsIn: (?Episode)[], // The movies this droid appears in
+};
+
+//==============================================================
+// START Enums and Input Objects
+// All enums and input objects are included in every output file
+// for now, but this will be changed soon.
+// TODO: Link to issue to fix this.
+//==============================================================
+
+// The episodes in the Star Wars trilogy
+export enum Episode {
+  NEWHOPE = \\"NEWHOPE\\",
+  EMPIRE = \\"EMPIRE\\",
+  JEDI = \\"JEDI\\",
+}
+
+//==============================================================
+// END Enums and Input Objects
+//==============================================================",
+  },
+  "__generated__/humanFragment.js": TypescriptGeneratedFile {
+    "fileContents": "
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL fragment: humanFragment
+// ====================================================
+
+export type humanFragment_friends = {
+  __typename: \\"Human\\",
+  name: string, // What this human calls themselves
+} | {
+  __typename: \\"Droid\\",
+  id: string,   // The ID of the droid
+};
+
+export type humanFragment = {
+  __typename: \\"Human\\",
+  homePlanet: string | undefined | null,             // The home planet of the human, or null if unknown
+  friends: humanFragment_friends | undefined | null, // This human's friends, or an empty list if they have none
+};
+
+//==============================================================
+// START Enums and Input Objects
+// All enums and input objects are included in every output file
+// for now, but this will be changed soon.
+// TODO: Link to issue to fix this.
+//==============================================================
+
+// The episodes in the Star Wars trilogy
+export enum Episode {
+  NEWHOPE = \\"NEWHOPE\\",
+  EMPIRE = \\"EMPIRE\\",
+  JEDI = \\"JEDI\\",
+}
+
+//==============================================================
+// END Enums and Input Objects
+//==============================================================",
+  },
+}
+`;
+
+exports[`Typescript codeGeneration fragment with fragment spreads 1`] = `
+Object {
+  "__generated__/anotherFragment.js": TypescriptGeneratedFile {
+    "fileContents": "
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL fragment: anotherFragment
+// ====================================================
+
+export type anotherFragment = {
+  __typename: \\"Human\\" | \\"Droid\\",
+  id: string,   // The ID of the character
+  name: string, // The name of the character
+};
+
+//==============================================================
+// START Enums and Input Objects
+// All enums and input objects are included in every output file
+// for now, but this will be changed soon.
+// TODO: Link to issue to fix this.
+//==============================================================
+
+//==============================================================
+// END Enums and Input Objects
+//==============================================================",
+  },
+  "__generated__/simpleFragment.js": TypescriptGeneratedFile {
+    "fileContents": "
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL fragment: simpleFragment
+// ====================================================
+
+export type simpleFragment = {
+  __typename: \\"Human\\" | \\"Droid\\",
+  name: string, // The name of the character
+};
+
+//==============================================================
+// START Enums and Input Objects
+// All enums and input objects are included in every output file
+// for now, but this will be changed soon.
+// TODO: Link to issue to fix this.
+//==============================================================
+
+//==============================================================
+// END Enums and Input Objects
+//==============================================================",
+  },
+}
+`;
+
+exports[`Typescript codeGeneration fragment with fragment spreads with inline fragment 1`] = `
+Object {
+  "__generated__/anotherFragment.js": TypescriptGeneratedFile {
+    "fileContents": "
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL fragment: anotherFragment
+// ====================================================
+
+export type anotherFragment_Droid = {
+  __typename: \\"Droid\\",
+  id: string,   // The ID of the droid
+  name: string, // What others call this droid
+};
+
+export type anotherFragment_Human = {
+  __typename: \\"Human\\",
+  id: string,              // The ID of the human
+  name: string,            // What this human calls themselves
+  appearsIn: (?Episode)[], // The movies this human appears in
+};
+
+export type anotherFragment = anotherFragment_Droid | anotherFragment_Human;
+
+//==============================================================
+// START Enums and Input Objects
+// All enums and input objects are included in every output file
+// for now, but this will be changed soon.
+// TODO: Link to issue to fix this.
+//==============================================================
+
+// The episodes in the Star Wars trilogy
+export enum Episode {
+  NEWHOPE = \\"NEWHOPE\\",
+  EMPIRE = \\"EMPIRE\\",
+  JEDI = \\"JEDI\\",
+}
+
+//==============================================================
+// END Enums and Input Objects
+//==============================================================",
+  },
+  "__generated__/simpleFragment.js": TypescriptGeneratedFile {
+    "fileContents": "
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL fragment: simpleFragment
+// ====================================================
+
+export type simpleFragment = {
+  __typename: \\"Human\\" | \\"Droid\\",
+  name: string, // The name of the character
+};
+
+//==============================================================
+// START Enums and Input Objects
+// All enums and input objects are included in every output file
+// for now, but this will be changed soon.
+// TODO: Link to issue to fix this.
+//==============================================================
+
+// The episodes in the Star Wars trilogy
+export enum Episode {
+  NEWHOPE = \\"NEWHOPE\\",
+  EMPIRE = \\"EMPIRE\\",
+  JEDI = \\"JEDI\\",
+}
+
+//==============================================================
+// END Enums and Input Objects
+//==============================================================",
+  },
+}
+`;
+
+exports[`Typescript codeGeneration inline fragment 1`] = `
+Object {
+  "__generated__/HeroInlineFragment.js": TypescriptGeneratedFile {
+    "fileContents": "
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL query operation: HeroInlineFragment
+// ====================================================
+
+export type HeroInlineFragment_hero = {
+  __typename: \\"Human\\" | \\"Droid\\",
+  name: string, // The name of the character
+  id: string,   // The ID of the character
+};
+
+export type HeroInlineFragment = {
+  hero: HeroInlineFragment_hero | undefined | null
+};
+
+//==============================================================
+// START Enums and Input Objects
+// All enums and input objects are included in every output file
+// for now, but this will be changed soon.
+// TODO: Link to issue to fix this.
+//==============================================================
+
+// The episodes in the Star Wars trilogy
+export enum Episode {
+  NEWHOPE = \\"NEWHOPE\\",
+  EMPIRE = \\"EMPIRE\\",
+  JEDI = \\"JEDI\\",
+}
+
+//==============================================================
+// END Enums and Input Objects
+//==============================================================",
+  },
+}
+`;
+
+exports[`Typescript codeGeneration inline fragment on type conditions 1`] = `
+Object {
+  "__generated__/HeroName.js": TypescriptGeneratedFile {
+    "fileContents": "
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL query operation: HeroName
+// ====================================================
+
+export type HeroName_hero_Human_friends = {
+  __typename: \\"Human\\" | \\"Droid\\",
+  name: string, // The name of the character
+};
+
+export type HeroName_hero = {
+  __typename: \\"Human\\",
+  name: string,                                            // What this human calls themselves
+  id: string,                                              // The ID of the human
+  homePlanet: string | undefined | null,                   // The home planet of the human, or null if unknown
+  friends: HeroName_hero_Human_friends | undefined | null, // This human's friends, or an empty list if they have none
+} | {
+  __typename: \\"Droid\\",
+  name: string,                                            // What others call this droid
+  id: string,                                              // The ID of the droid
+  appearsIn: (?Episode)[],                                 // The movies this droid appears in
+};
+
+export type HeroName = {
+  hero: HeroName_hero | undefined | null
+};
+
+//==============================================================
+// START Enums and Input Objects
+// All enums and input objects are included in every output file
+// for now, but this will be changed soon.
+// TODO: Link to issue to fix this.
+//==============================================================
+
+// The episodes in the Star Wars trilogy
+export enum Episode {
+  NEWHOPE = \\"NEWHOPE\\",
+  EMPIRE = \\"EMPIRE\\",
+  JEDI = \\"JEDI\\",
+}
+
+//==============================================================
+// END Enums and Input Objects
+//==============================================================",
+  },
+}
+`;
+
+exports[`Typescript codeGeneration inline fragment on type conditions with differing inner fields 1`] = `
+Object {
+  "__generated__/HeroName.js": TypescriptGeneratedFile {
+    "fileContents": "
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL query operation: HeroName
+// ====================================================
+
+export type HeroName_hero_Human_friends = {
+  __typename: \\"Human\\" | \\"Droid\\",
+  name: string, // The name of the character
+};
+
+export type HeroName_hero_Droid_friends = {
+  __typename: \\"Human\\" | \\"Droid\\",
+  id: string, // The ID of the character
+};
+
+export type HeroName_hero = {
+  __typename: \\"Human\\",
+  name: string,                                            // What this human calls themselves
+  id: string,                                              // The ID of the human
+  homePlanet: string | undefined | null,                   // The home planet of the human, or null if unknown
+  friends: HeroName_hero_Human_friends | undefined | null, // This human's friends, or an empty list if they have none
+} | {
+  __typename: \\"Droid\\",
+  name: string,                                            // What others call this droid
+  id: string,                                              // The ID of the droid
+  appearsIn: (?Episode)[],                                 // The movies this droid appears in
+  friends: HeroName_hero_Droid_friends | undefined | null, // This droid's friends, or an empty list if they have none
+};
+
+export type HeroName = {
+  hero: HeroName_hero | undefined | null
+};
+
+//==============================================================
+// START Enums and Input Objects
+// All enums and input objects are included in every output file
+// for now, but this will be changed soon.
+// TODO: Link to issue to fix this.
+//==============================================================
+
+// The episodes in the Star Wars trilogy
+export enum Episode {
+  NEWHOPE = \\"NEWHOPE\\",
+  EMPIRE = \\"EMPIRE\\",
+  JEDI = \\"JEDI\\",
+}
+
+//==============================================================
+// END Enums and Input Objects
+//==============================================================",
+  },
+}
+`;
+
+exports[`Typescript codeGeneration multiple files 1`] = `"/some/file/__generated__/HeroName.js"`;
+
+exports[`Typescript codeGeneration multiple files 2`] = `
+TypescriptGeneratedFile {
+  "fileContents": "
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL query operation: HeroName
+// ====================================================
+
+export type HeroName_hero = {
+  __typename: \\"Human\\" | \\"Droid\\",
+  name: string, // The name of the character
+  id: string,   // The ID of the character
+};
+
+export type HeroName = {
+  hero: HeroName_hero | undefined | null
+};
+
+//==============================================================
+// START Enums and Input Objects
+// All enums and input objects are included in every output file
+// for now, but this will be changed soon.
+// TODO: Link to issue to fix this.
+//==============================================================
+
+// The episodes in the Star Wars trilogy
+export enum Episode {
+  NEWHOPE = \\"NEWHOPE\\",
+  EMPIRE = \\"EMPIRE\\",
+  JEDI = \\"JEDI\\",
+}
+
+// The input object sent when someone is creating a new review
+type ReviewInput = {
+  stars: number,
+  commentary: string | undefined | null,
+  favorite_color: ColorInput | undefined | null,
+};
+
+// The input object sent when passing in a color
+type ColorInput = {
+  red: number,
+  green: number,
+  blue: number,
+};
+
+//==============================================================
+// END Enums and Input Objects
+//==============================================================",
+}
+`;
+
+exports[`Typescript codeGeneration multiple files 3`] = `"/some/file/__generated__/SomeOther.js"`;
+
+exports[`Typescript codeGeneration multiple files 4`] = `
+TypescriptGeneratedFile {
+  "fileContents": "
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL query operation: SomeOther
+// ====================================================
+
+export type SomeOther_hero = {
+  __typename: \\"Human\\" | \\"Droid\\",
+  name: string,            // The name of the character
+  appearsIn: (?Episode)[], // The movies this character appears in
+};
+
+export type SomeOther = {
+  hero: SomeOther_hero | undefined | null
+};
+
+//==============================================================
+// START Enums and Input Objects
+// All enums and input objects are included in every output file
+// for now, but this will be changed soon.
+// TODO: Link to issue to fix this.
+//==============================================================
+
+// The episodes in the Star Wars trilogy
+export enum Episode {
+  NEWHOPE = \\"NEWHOPE\\",
+  EMPIRE = \\"EMPIRE\\",
+  JEDI = \\"JEDI\\",
+}
+
+// The input object sent when someone is creating a new review
+type ReviewInput = {
+  stars: number,
+  commentary: string | undefined | null,
+  favorite_color: ColorInput | undefined | null,
+};
+
+// The input object sent when passing in a color
+type ColorInput = {
+  red: number,
+  green: number,
+  blue: number,
+};
+
+//==============================================================
+// END Enums and Input Objects
+//==============================================================",
+}
+`;
+
+exports[`Typescript codeGeneration multiple files 5`] = `"__generated__/ReviewMovie.js"`;
+
+exports[`Typescript codeGeneration multiple files 6`] = `
+TypescriptGeneratedFile {
+  "fileContents": "
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL mutation operation: ReviewMovie
+// ====================================================
+
+export type ReviewMovie_createReview = {
+  __typename: \\"Review\\",
+  stars: number,                         // The number of stars this review gave, 1-5
+  commentary: string | undefined | null, // Comment about the movie
+};
+
+export type ReviewMovie = {
+  createReview: ReviewMovie_createReview | undefined | null
+};
+
+//==============================================================
+// START Enums and Input Objects
+// All enums and input objects are included in every output file
+// for now, but this will be changed soon.
+// TODO: Link to issue to fix this.
+//==============================================================
+
+// The episodes in the Star Wars trilogy
+export enum Episode {
+  NEWHOPE = \\"NEWHOPE\\",
+  EMPIRE = \\"EMPIRE\\",
+  JEDI = \\"JEDI\\",
+}
+
+// The input object sent when someone is creating a new review
+type ReviewInput = {
+  stars: number,
+  commentary: string | undefined | null,
+  favorite_color: ColorInput | undefined | null,
+};
+
+// The input object sent when passing in a color
+type ColorInput = {
+  red: number,
+  green: number,
+  blue: number,
+};
+
+//==============================================================
+// END Enums and Input Objects
+//==============================================================",
+}
+`;
+
+exports[`Typescript codeGeneration multiple files 7`] = `"/some/file/__generated__/someFragment.js"`;
+
+exports[`Typescript codeGeneration multiple files 8`] = `
+TypescriptGeneratedFile {
+  "fileContents": "
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL fragment: someFragment
+// ====================================================
+
+export type someFragment = {
+  __typename: \\"Human\\" | \\"Droid\\",
+  appearsIn: (?Episode)[], // The movies this character appears in
+};
+
+//==============================================================
+// START Enums and Input Objects
+// All enums and input objects are included in every output file
+// for now, but this will be changed soon.
+// TODO: Link to issue to fix this.
+//==============================================================
+
+// The episodes in the Star Wars trilogy
+export enum Episode {
+  NEWHOPE = \\"NEWHOPE\\",
+  EMPIRE = \\"EMPIRE\\",
+  JEDI = \\"JEDI\\",
+}
+
+// The input object sent when someone is creating a new review
+type ReviewInput = {
+  stars: number,
+  commentary: string | undefined | null,
+  favorite_color: ColorInput | undefined | null,
+};
+
+// The input object sent when passing in a color
+type ColorInput = {
+  red: number,
+  green: number,
+  blue: number,
+};
+
+//==============================================================
+// END Enums and Input Objects
+//==============================================================",
+}
+`;
+
+exports[`Typescript codeGeneration query with fragment spreads 1`] = `
+Object {
+  "__generated__/HeroFragment.js": TypescriptGeneratedFile {
+    "fileContents": "
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL query operation: HeroFragment
+// ====================================================
+
+export type HeroFragment_hero = {
+  __typename: \\"Human\\" | \\"Droid\\",
+  name: string, // The name of the character
+  id: string,   // The ID of the character
+};
+
+export type HeroFragment = {
+  hero: HeroFragment_hero | undefined | null
+};
+
+//==============================================================
+// START Enums and Input Objects
+// All enums and input objects are included in every output file
+// for now, but this will be changed soon.
+// TODO: Link to issue to fix this.
+//==============================================================
+
+// The episodes in the Star Wars trilogy
+export enum Episode {
+  NEWHOPE = \\"NEWHOPE\\",
+  EMPIRE = \\"EMPIRE\\",
+  JEDI = \\"JEDI\\",
+}
+
+//==============================================================
+// END Enums and Input Objects
+//==============================================================",
+  },
+  "__generated__/simpleFragment.js": TypescriptGeneratedFile {
+    "fileContents": "
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL fragment: simpleFragment
+// ====================================================
+
+export type simpleFragment = {
+  __typename: \\"Human\\" | \\"Droid\\",
+  name: string, // The name of the character
+};
+
+//==============================================================
+// START Enums and Input Objects
+// All enums and input objects are included in every output file
+// for now, but this will be changed soon.
+// TODO: Link to issue to fix this.
+//==============================================================
+
+// The episodes in the Star Wars trilogy
+export enum Episode {
+  NEWHOPE = \\"NEWHOPE\\",
+  EMPIRE = \\"EMPIRE\\",
+  JEDI = \\"JEDI\\",
+}
+
+//==============================================================
+// END Enums and Input Objects
+//==============================================================",
+  },
+}
+`;
+
+exports[`Typescript codeGeneration simple fragment 1`] = `
+Object {
+  "__generated__/SimpleFragment.js": TypescriptGeneratedFile {
+    "fileContents": "
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL fragment: SimpleFragment
+// ====================================================
+
+export type SimpleFragment = {
+  __typename: \\"Human\\" | \\"Droid\\",
+  name: string, // The name of the character
+};
+
+//==============================================================
+// START Enums and Input Objects
+// All enums and input objects are included in every output file
+// for now, but this will be changed soon.
+// TODO: Link to issue to fix this.
+//==============================================================
+
+//==============================================================
+// END Enums and Input Objects
+//==============================================================",
+  },
+}
+`;
+
+exports[`Typescript codeGeneration simple hero query 1`] = `
+Object {
+  "__generated__/HeroName.js": TypescriptGeneratedFile {
+    "fileContents": "
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL query operation: HeroName
+// ====================================================
+
+export type HeroName_hero = {
+  __typename: \\"Human\\" | \\"Droid\\",
+  name: string, // The name of the character
+  id: string,   // The ID of the character
+};
+
+export type HeroName = {
+  hero: HeroName_hero | undefined | null
+};
+
+//==============================================================
+// START Enums and Input Objects
+// All enums and input objects are included in every output file
+// for now, but this will be changed soon.
+// TODO: Link to issue to fix this.
+//==============================================================
+
+// The episodes in the Star Wars trilogy
+export enum Episode {
+  NEWHOPE = \\"NEWHOPE\\",
+  EMPIRE = \\"EMPIRE\\",
+  JEDI = \\"JEDI\\",
+}
+
+//==============================================================
+// END Enums and Input Objects
+//==============================================================",
+  },
+}
+`;
+
+exports[`Typescript codeGeneration simple mutation 1`] = `
+Object {
+  "__generated__/ReviewMovie.js": TypescriptGeneratedFile {
+    "fileContents": "
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL mutation operation: ReviewMovie
+// ====================================================
+
+export type ReviewMovie_createReview = {
+  __typename: \\"Review\\",
+  stars: number,                         // The number of stars this review gave, 1-5
+  commentary: string | undefined | null, // Comment about the movie
+};
+
+export type ReviewMovie = {
+  createReview: ReviewMovie_createReview | undefined | null
+};
+
+//==============================================================
+// START Enums and Input Objects
+// All enums and input objects are included in every output file
+// for now, but this will be changed soon.
+// TODO: Link to issue to fix this.
+//==============================================================
+
+// The episodes in the Star Wars trilogy
+export enum Episode {
+  NEWHOPE = \\"NEWHOPE\\",
+  EMPIRE = \\"EMPIRE\\",
+  JEDI = \\"JEDI\\",
+}
+
+// The input object sent when someone is creating a new review
+type ReviewInput = {
+  stars: number,
+  commentary?: string | undefined | null,
+  favorite_color?: ColorInput | undefined | null,
+};
+
+// The input object sent when passing in a color
+type ColorInput = {
+  red: number,
+  green: number,
+  blue: number,
+};
+
+//==============================================================
+// END Enums and Input Objects
+//==============================================================",
+  },
+}
+`;
diff --git a/src/javascript/typescript/__tests__/codeGeneration.ts b/src/javascript/typescript/__tests__/codeGeneration.ts
new file mode 100644
index 00000000..1879d782
--- /dev/null
+++ b/src/javascript/typescript/__tests__/codeGeneration.ts
@@ -0,0 +1,257 @@
+import { parse } from 'graphql';
+
+import { loadSchema } from '../../../loading';
+const schema = loadSchema(require.resolve('../../../../test/fixtures/starwars/schema.json'));
+
+import {
+  compileToIR,
+  CompilerOptions,
+  CompilerContext,
+} from '../../../compiler';
+
+import { generateSource } from '../codeGeneration';
+
+function compile(
+  source: string,
+  options: CompilerOptions = {
+    mergeInFieldsFromFragmentSpreads: true,
+    addTypename: true
+  }
+): CompilerContext {
+  const document = parse(source);
+  return compileToIR(schema, document, options);
+}
+
+describe('Typescript codeGeneration', () => {
+  test('multiple files', () => {
+    const context = compile(`
+      query HeroName($episode: Episode) {
+        hero(episode: $episode) {
+          name
+          id
+        }
+      }
+
+      query SomeOther($episode: Episode) {
+        hero(episode: $episode) {
+          name
+          ...someFragment
+        }
+      }
+
+      fragment someFragment on Character {
+        appearsIn
+      }
+
+      mutation ReviewMovie($episode: Episode, $review: ReviewInput) {
+        createReview(episode: $episode, review: $review) {
+          stars
+          commentary
+        }
+      }
+    `);
+    context.operations["HeroName"].filePath = '/some/file/ComponentA.js';
+    context.operations["SomeOther"].filePath = '/some/file/ComponentB.js';
+    context.fragments['someFragment'].filePath = '/some/file/ComponentB.js';
+    const output = generateSource(context);
+    expect(output).toBeInstanceOf(Object);
+    Object.keys(output)
+      .forEach((filePath) => {
+        expect(filePath).toMatchSnapshot();
+        expect(output[filePath]).toMatchSnapshot();
+      });
+  });
+
+  test('simple hero query', () => {
+    const context = compile(`
+      query HeroName($episode: Episode) {
+        hero(episode: $episode) {
+          name
+          id
+        }
+      }
+    `);
+
+    const output = generateSource(context);
+    expect(output).toMatchSnapshot();
+  });
+
+  test.only('simple mutation', () => {
+    const context = compile(`
+      mutation ReviewMovie($episode: Episode, $review: ReviewInput) {
+        createReview(episode: $episode, review: $review) {
+          stars
+          commentary
+        }
+      }
+    `);
+
+    const output = generateSource(context);
+    expect(output).toMatchSnapshot();
+  });
+
+  test('simple fragment', () => {
+    const context = compile(`
+      fragment SimpleFragment on Character{
+        name
+      }
+    `);
+
+    const output = generateSource(context);
+    expect(output).toMatchSnapshot();
+  });
+
+  test('fragment with fragment spreads', () => {
+    const context = compile(`
+      fragment simpleFragment on Character {
+        name
+      }
+
+      fragment anotherFragment on Character {
+        id
+        ...simpleFragment
+      }
+    `);
+
+    const output = generateSource(context);
+    expect(output).toMatchSnapshot();
+  });
+
+  test('fragment with fragment spreads with inline fragment', () => {
+    const context = compile(`
+      fragment simpleFragment on Character {
+        name
+      }
+
+      fragment anotherFragment on Character {
+        id
+        ...simpleFragment
+
+        ... on Human {
+          appearsIn
+        }
+      }
+    `);
+
+    const output = generateSource(context);
+    expect(output).toMatchSnapshot();
+  });
+
+  test('query with fragment spreads', () => {
+    const context = compile(`
+      fragment simpleFragment on Character {
+        name
+      }
+
+      query HeroFragment($episode: Episode) {
+        hero(episode: $episode) {
+          ...simpleFragment
+          id
+        }
+      }
+    `);
+
+    const output = generateSource(context);
+    expect(output).toMatchSnapshot();
+  });
+
+  test('inline fragment', () => {
+    const context = compile(`
+      query HeroInlineFragment($episode: Episode) {
+        hero(episode: $episode) {
+          ... on Character {
+            name
+          }
+          id
+        }
+      }
+    `);
+
+    const output = generateSource(context);
+    expect(output).toMatchSnapshot();
+  })
+
+  test('inline fragment on type conditions', () => {
+    const context = compile(`
+      query HeroName($episode: Episode) {
+        hero(episode: $episode) {
+          name
+          id
+
+          ... on Human {
+            homePlanet
+            friends {
+              name
+            }
+          }
+
+          ... on Droid {
+            appearsIn
+          }
+        }
+      }
+    `);
+    const output = generateSource(context);
+    expect(output).toMatchSnapshot();
+  });
+
+  test('inline fragment on type conditions with differing inner fields', () => {
+    const context = compile(`
+      query HeroName($episode: Episode) {
+        hero(episode: $episode) {
+          name
+          id
+
+          ... on Human {
+            homePlanet
+            friends {
+              name
+            }
+          }
+
+          ... on Droid {
+            appearsIn
+            friends {
+              id
+            }
+          }
+        }
+      }
+    `);
+
+    const output = generateSource(context);
+    expect(output).toMatchSnapshot();
+  });
+
+  test('fragment spreads with inline fragments', () => {
+    const context = compile(`
+      query HeroName($episode: Episode) {
+        hero(episode: $episode) {
+          name
+          id
+          ...humanFragment
+          ...droidFragment
+        }
+      }
+
+      fragment humanFragment on Human {
+        homePlanet
+        friends {
+          ... on Human {
+            name
+          }
+
+          ... on Droid {
+            id
+          }
+        }
+      }
+
+      fragment droidFragment on Droid {
+        appearsIn
+      }
+    `);
+    const output = generateSource(context);
+    expect(output).toMatchSnapshot();
+  });
+});
diff --git a/src/javascript/typescript/__tests__/helpers.ts b/src/javascript/typescript/__tests__/helpers.ts
new file mode 100644
index 00000000..6f8e757b
--- /dev/null
+++ b/src/javascript/typescript/__tests__/helpers.ts
@@ -0,0 +1,414 @@
+import {
+  GraphQLString,
+  GraphQLInt,
+  GraphQLFloat,
+  GraphQLBoolean,
+  GraphQLID,
+  GraphQLNonNull,
+  GraphQLList,
+  GraphQLScalarType,
+} from 'graphql';
+
+import * as t from 'babel-types';
+
+import { createTypeAnnotationFromGraphQLTypeFunction } from '../helpers';
+
+const typeAnnotationFromGraphQLType = createTypeAnnotationFromGraphQLTypeFunction({
+  passthroughCustomScalars: false
+});
+
+describe('Flow typeAnnotationFromGraphQLType', () => {
+  test('String', () => {
+    expect(typeAnnotationFromGraphQLType(GraphQLString))
+      .toMatchObject(
+        t.nullableTypeAnnotation(
+          t.stringTypeAnnotation()
+        )
+      );
+  });
+
+  test('Int', () => {
+    expect(typeAnnotationFromGraphQLType(GraphQLInt))
+      .toMatchObject(
+        t.nullableTypeAnnotation(
+          t.numberTypeAnnotation()
+        )
+      );
+  });
+
+  test('Float', () => {
+    expect(typeAnnotationFromGraphQLType(GraphQLFloat))
+      .toMatchObject(
+        t.nullableTypeAnnotation(
+          t.numberTypeAnnotation()
+        )
+      );
+  });
+
+  test('Boolean', () => {
+    expect(typeAnnotationFromGraphQLType(GraphQLBoolean))
+      .toMatchObject(
+        t.nullableTypeAnnotation(
+          t.booleanTypeAnnotation()
+        )
+      );
+  });
+
+  test('ID', () => {
+    expect(typeAnnotationFromGraphQLType(GraphQLID))
+      .toMatchObject(
+        t.nullableTypeAnnotation(
+          t.stringTypeAnnotation()
+        )
+      );
+  });
+
+  test('String!', () => {
+    expect(
+      typeAnnotationFromGraphQLType(new GraphQLNonNull(GraphQLString))
+    ).toMatchObject(t.stringTypeAnnotation());
+  });
+
+  test('Int!', () => {
+    expect(
+      typeAnnotationFromGraphQLType(new GraphQLNonNull(GraphQLInt))
+    ).toMatchObject(t.numberTypeAnnotation());
+  });
+
+  test('Float!', () => {
+    expect(
+      typeAnnotationFromGraphQLType(new GraphQLNonNull(GraphQLFloat))
+    ).toMatchObject(t.numberTypeAnnotation());
+  });
+
+  test('Boolean!', () => {
+    expect(
+      typeAnnotationFromGraphQLType(new GraphQLNonNull(GraphQLBoolean))
+    ).toMatchObject(t.booleanTypeAnnotation());
+  });
+
+  test('ID!', () => {
+    expect(
+      typeAnnotationFromGraphQLType(new GraphQLNonNull(GraphQLID))
+    ).toMatchObject(t.stringTypeAnnotation());
+  });
+
+  // TODO: Test GenericTypeAnnotation
+
+  test('[String]', () => {
+    expect(
+      typeAnnotationFromGraphQLType(new GraphQLList(GraphQLString))
+    ).toMatchObject(
+        t.nullableTypeAnnotation(
+          t.arrayTypeAnnotation(
+            t.nullableTypeAnnotation(
+              t.stringTypeAnnotation()
+            )
+          )
+        )
+      )
+  });
+
+  test('[Int]', () => {
+    expect(
+      typeAnnotationFromGraphQLType(new GraphQLList(GraphQLInt))
+    ).toMatchObject(
+        t.nullableTypeAnnotation(
+          t.arrayTypeAnnotation(
+            t.nullableTypeAnnotation(
+              t.numberTypeAnnotation()
+            )
+          )
+        )
+      )
+  });
+
+  test('[Float]', () => {
+    expect(
+      typeAnnotationFromGraphQLType(new GraphQLList(GraphQLFloat))
+    ).toMatchObject(
+        t.nullableTypeAnnotation(
+          t.arrayTypeAnnotation(
+            t.nullableTypeAnnotation(
+              t.numberTypeAnnotation()
+            )
+          )
+        )
+      )
+  });
+
+  test('[Boolean]', () => {
+    expect(
+      typeAnnotationFromGraphQLType(new GraphQLList(GraphQLBoolean))
+    ).toMatchObject(
+        t.nullableTypeAnnotation(
+          t.arrayTypeAnnotation(
+            t.nullableTypeAnnotation(
+              t.booleanTypeAnnotation()
+            )
+          )
+        )
+      )
+  });
+
+  test('[ID]', () => {
+    expect(
+      typeAnnotationFromGraphQLType(new GraphQLList(GraphQLID))
+    ).toMatchObject(
+        t.nullableTypeAnnotation(
+          t.arrayTypeAnnotation(
+            t.nullableTypeAnnotation(
+              t.stringTypeAnnotation()
+            )
+          )
+        )
+      )
+  })
+
+  test('[String]!', () => {
+    expect(
+      typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(GraphQLString)))
+    ).toMatchObject(
+        t.arrayTypeAnnotation(
+          t.nullableTypeAnnotation(
+            t.stringTypeAnnotation()
+          )
+        )
+      )
+  });
+
+  test('[Int]!', () => {
+    expect(
+      typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(GraphQLInt)))
+    ).toMatchObject(
+        t.arrayTypeAnnotation(
+          t.nullableTypeAnnotation(
+            t.numberTypeAnnotation()
+          )
+        )
+      )
+  });
+  test('[Float]!', () => {
+    expect(
+      typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(GraphQLFloat)))
+    ).toMatchObject(
+        t.arrayTypeAnnotation(
+          t.nullableTypeAnnotation(
+            t.numberTypeAnnotation()
+          )
+        )
+      )
+  });
+
+  test('[Boolean]!', () => {
+    expect(
+      typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(GraphQLBoolean)))
+    ).toMatchObject(
+        t.arrayTypeAnnotation(
+          t.nullableTypeAnnotation(
+            t.booleanTypeAnnotation()
+          )
+        )
+      )
+  });
+
+  test('[ID]!', () => {
+    expect(
+      typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(GraphQLID)))
+    ).toMatchObject(
+        t.arrayTypeAnnotation(
+          t.nullableTypeAnnotation(
+            t.stringTypeAnnotation()
+          )
+        )
+      )
+  });
+
+  test('[String!]', () => {
+    expect(
+      typeAnnotationFromGraphQLType(new GraphQLList(new GraphQLNonNull(GraphQLString)))
+    ).toMatchObject(
+      t.nullableTypeAnnotation(
+          t.arrayTypeAnnotation(
+            t.stringTypeAnnotation()
+          )
+        )
+      )
+  });
+
+  test('[Int!]', () => {
+    expect(
+      typeAnnotationFromGraphQLType(new GraphQLList(new GraphQLNonNull((GraphQLInt))))
+    ).toMatchObject(
+      t.nullableTypeAnnotation(
+          t.arrayTypeAnnotation(
+            t.numberTypeAnnotation()
+          )
+        )
+      )
+  })
+
+  test('[Float!]', () => {
+    expect(
+      typeAnnotationFromGraphQLType(new GraphQLList(new GraphQLNonNull(GraphQLFloat)))
+    ).toMatchObject(
+        t.nullableTypeAnnotation(
+          t.arrayTypeAnnotation(
+            t.numberTypeAnnotation()
+          )
+        )
+      )
+  });
+
+  test('[Boolean!]', () => {
+    expect(
+      typeAnnotationFromGraphQLType(new GraphQLList(new GraphQLNonNull(GraphQLBoolean)))
+    ).toMatchObject(
+        t.nullableTypeAnnotation(
+          t.arrayTypeAnnotation(
+            t.booleanTypeAnnotation()
+          )
+        )
+      )
+  });
+
+  test('[ID!]', () => {
+    expect(
+      typeAnnotationFromGraphQLType(new GraphQLList(new GraphQLNonNull(GraphQLID)))
+    ).toMatchObject(
+        t.nullableTypeAnnotation(
+          t.arrayTypeAnnotation(
+            t.stringTypeAnnotation()
+          )
+        )
+      )
+  });
+
+  test('[String!]!', () => {
+    expect(
+      typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLString))))
+    ).toMatchObject(
+        t.arrayTypeAnnotation(
+          t.stringTypeAnnotation()
+        )
+      )
+  });
+
+  test('[Int!]!', () => {
+    expect(
+      typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLInt))))
+    ).toMatchObject(
+        t.arrayTypeAnnotation(
+          t.numberTypeAnnotation()
+        )
+      )
+  });
+
+  test('[Float!]!', () => {
+    expect(
+      typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLFloat))))
+    ).toMatchObject(
+        t.arrayTypeAnnotation(
+          t.numberTypeAnnotation()
+        )
+      )
+  });
+
+  test('[Boolean!]!', () => {
+    expect(
+      typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLBoolean))))
+    ).toMatchObject(
+        t.arrayTypeAnnotation(
+          t.booleanTypeAnnotation()
+        )
+      )
+  });
+
+  test('[ID!]!', () => {
+    expect(
+      typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLID))))
+    ).toMatchObject(
+        t.arrayTypeAnnotation(
+          t.stringTypeAnnotation()
+        )
+      )
+  });
+
+  test('[[String]]', () => {
+    expect(
+      typeAnnotationFromGraphQLType(new GraphQLList(new GraphQLList(GraphQLString)))
+    ).toMatchObject(
+        t.nullableTypeAnnotation(
+          t.arrayTypeAnnotation(
+            t.nullableTypeAnnotation(
+              t.arrayTypeAnnotation(
+                t.nullableTypeAnnotation(
+                  t.stringTypeAnnotation()
+                )
+              )
+            )
+          )
+        )
+      )
+  });
+
+  test('[[String]]!', () => {
+    expect(
+      typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(new GraphQLList(GraphQLString))))
+    ).toMatchObject(
+        t.arrayTypeAnnotation(
+          t.nullableTypeAnnotation(
+            t.arrayTypeAnnotation(
+              t.nullableTypeAnnotation(
+                t.stringTypeAnnotation()
+              )
+            )
+          )
+        )
+      )
+  });
+
+  test('Custom Scalar', () => {
+    const OddType = new GraphQLScalarType({
+      name: 'Odd',
+      serialize(value) {
+        return value % 2 === 1 ? value : null
+      }
+    });
+
+    expect(
+      typeAnnotationFromGraphQLType(OddType)
+    ).toMatchObject(
+      t.nullableTypeAnnotation(
+        t.genericTypeAnnotation(t.identifier('Odd'))
+      )
+    )
+  });
+});
+
+describe('passthrough custom scalars', () => {
+  let getTypeAnnotation: Function;
+
+  beforeAll(() => {
+    getTypeAnnotation = createTypeAnnotationFromGraphQLTypeFunction({
+      passthroughCustomScalars: true
+    });
+  });
+
+  test('Custom Scalar', () => {
+    const OddType = new GraphQLScalarType({
+      name: 'Odd',
+      serialize(value) {
+        return value % 2 === 1 ? value : null
+      }
+    });
+
+    expect(
+      getTypeAnnotation(OddType)
+    ).toMatchObject(
+      t.nullableTypeAnnotation(
+        t.anyTypeAnnotation()
+      )
+    )
+  });
+});