diff --git a/reviewboard/static/rb/js/common/resources/index.ts b/reviewboard/static/rb/js/common/resources/index.ts
index 352e788c0b7f2bf0ad5fdf34e86413f3266be9c1..bd120cacfe45a29e6b380d7d74bd3d253d674f37 100644
--- a/reviewboard/static/rb/js/common/resources/index.ts
+++ b/reviewboard/static/rb/js/common/resources/index.ts
@@ -1,6 +1,7 @@
+export { ResourceCollection } from './collections/resourceCollection';
 export { BaseResource } from './models/baseResourceModel';
 export { DraftResourceModelMixin } from './models/draftResourceModelMixin';
-export { ResourceCollection } from './collections/resourceCollection';
+export { DraftReview } from './models/draftReviewModel';
 export { Review } from './models/reviewModel';
 export { ReviewReply } from './models/reviewReplyModel';
 export * as JSONSerializers from './utils/serializers';
diff --git a/reviewboard/static/rb/js/common/resources/models/baseResourceModel.ts b/reviewboard/static/rb/js/common/resources/models/baseResourceModel.ts
index acb4f898712336bba37f35cb4a15d268677778a6..e357e15c523a6e723d3bcc797e514960e25cceb6 100644
--- a/reviewboard/static/rb/js/common/resources/models/baseResourceModel.ts
+++ b/reviewboard/static/rb/js/common/resources/models/baseResourceModel.ts
@@ -20,7 +20,12 @@ export interface ResourceLink {
 }
 
 
-/** Attributes for the BaseResource model. */
+/**
+ * Attributes for the BaseResource model.
+ *
+ * Version Added:
+ *     6.0
+ */
 export interface BaseResourceAttrs extends ModelAttributes {
     /** Extra data storage. */
     extraData: object;
@@ -38,6 +43,12 @@ export interface BaseResourceAttrs extends ModelAttributes {
 }
 
 
+/**
+ * Options for the ready operation.
+ *
+ * Version Added:
+ *     6.0
+ */
 interface ReadyOptions extends Backbone.PersistenceOptions {
     /** Data to send when fetching the object from the server. */
     data?: object;
@@ -52,12 +63,27 @@ interface ReadyOptions extends Backbone.PersistenceOptions {
 }
 
 
-interface SaveWithFilesOptions extends Backbone.ModelSaveOptions {
+/**
+ * Options for the save operation.
+ *
+ * Version Added:
+ *     6.0
+ */
+export interface SaveOptions extends Backbone.ModelSaveOptions {
     /** Additional attributes to include in the payload. */
-    attrs: {
+    attrs?: {
         [key: string]: unknown;
     };
+}
+
 
+/**
+ * Options for saving the resource with files.
+ *
+ * Version Added:
+ *     6.0
+ */
+interface SaveWithFilesOptions extends SaveOptions {
     /** The boundary to use when formatting multipart payloads. */
     boundary?: string;
 }
@@ -452,7 +478,7 @@ export class BaseResource<
      *     A promise which resolves when the operation is complete.
      */
     async save(
-        options: Backbone.ModelSaveOptions = {},
+        options: SaveOptions = {},
         context: object = undefined,
     ): Promise<JQuery.jqXHR> {
         if (_.isFunction(options.success) ||
diff --git a/reviewboard/static/rb/js/common/resources/models/reviewModel.ts b/reviewboard/static/rb/js/common/resources/models/reviewModel.ts
index 67b76dab4d086ac93cddef15d1a587b9ab6d5e66..3172270a1be6e1a947997989dd5564f1c19294ce 100644
--- a/reviewboard/static/rb/js/common/resources/models/reviewModel.ts
+++ b/reviewboard/static/rb/js/common/resources/models/reviewModel.ts
@@ -15,7 +15,7 @@ import { ReviewReply } from './reviewReplyModel';
  * Version Added:
  *     6.0
  */
-interface ReviewAttrs extends BaseResourceAttrs {
+export interface ReviewAttrs extends BaseResourceAttrs {
     /** The name of the review author. */
     authorName: string;
 
@@ -156,7 +156,9 @@ interface CreateDiffCommentOptions {
         'supportsExtraData',
     ],
 })
-export class Review extends BaseResource<ReviewAttrs> {
+export class Review<
+    TAttributes extends ReviewAttrs = ReviewAttrs
+> extends BaseResource<TAttributes> {
     /**
      * Return default values for the model attributes.
      *
@@ -164,7 +166,7 @@ export class Review extends BaseResource<ReviewAttrs> {
      *     ReviewAttrs:
      *     The attribute defaults.
      */
-    defaults(): ReviewAttrs {
+    defaults(): TAttributes {
         return _.defaults({
             'authorName': null,
             'bodyBottom': null,
@@ -237,9 +239,9 @@ export class Review extends BaseResource<ReviewAttrs> {
      */
     parseResourceData(
         rsp: ReviewResourceData,
-    ): Partial<ReviewAttrs> {
+    ): Partial<TAttributes> {
         const rawTextFields = rsp.raw_text_fields || rsp;
-        const data = super.parseResourceData(rsp) as Partial<ReviewAttrs>;
+        const data = super.parseResourceData(rsp) as Partial<TAttributes>;
 
         data.bodyTopRichText =
             (rawTextFields.body_top_text_type === 'markdown');
diff --git a/reviewboard/static/rb/js/common/resources/models/tests/index.ts b/reviewboard/static/rb/js/common/resources/models/tests/index.ts
index 5a47057696d7f9417169821f8c977677b95e76e2..a26af6368d8f09a9b470994ca46b70e36bc8bf06 100644
--- a/reviewboard/static/rb/js/common/resources/models/tests/index.ts
+++ b/reviewboard/static/rb/js/common/resources/models/tests/index.ts
@@ -1,3 +1,4 @@
 import './baseResourceModelTests';
+import './draftReviewModelTests';
 import './reviewModelTests';
 import './reviewReplyModelTests';
diff --git a/reviewboard/static/rb/js/resources/models/draftReviewModel.es6.js b/reviewboard/static/rb/js/common/resources/models/draftReviewModel.ts
similarity index 51%
rename from reviewboard/static/rb/js/resources/models/draftReviewModel.es6.js
rename to reviewboard/static/rb/js/common/resources/models/draftReviewModel.ts
index a74b19eecd2147da4e38be7414b5b5e5da53824c..ac9d6877f25845b20e04e6363d66f227625c7a3b 100644
--- a/reviewboard/static/rb/js/resources/models/draftReviewModel.es6.js
+++ b/reviewboard/static/rb/js/common/resources/models/draftReviewModel.ts
@@ -1,3 +1,30 @@
+/**
+ * A draft review.
+ */
+
+import { spina } from '@beanbag/spina';
+
+import * as JSONSerializers from '../utils/serializers';
+import { SaveOptions } from './baseResourceModel';
+import { DraftResourceModelMixin } from './draftResourceModelMixin';
+import { Review, ReviewAttrs } from './reviewModel';
+
+
+/**
+ * Attributes for the DraftReview model.
+ *
+ * Version Added:
+ *     6.0
+ */
+interface DraftReviewAttrs extends ReviewAttrs {
+    /** Whether to archive the review request after publishing the review. */
+    publishAndArchive: boolean;
+
+    /** Whether to limit e-mails to only the owner of the review request. */
+    publishToOwnerOnly: boolean;
+}
+
+
 /**
  * A draft review.
  *
@@ -6,27 +33,36 @@
  * special resource exists at /reviews/draft/ which will redirect to the
  * existing draft if one exists, and return 404 if not.
  */
-RB.DraftReview = RB.Review.extend(_.extend({
-    defaults: _.defaults({
-        publishAndArchive: false,
-        publishToOwnerOnly: false,
-    }, RB.Review.prototype.defaults()),
+@spina({
+    mixins: [DraftResourceModelMixin],
+    prototypeAttrs: [
+        'attrToJsonMap',
+        'serializedAttrs',
+        'serializers',
+    ],
+})
+export class DraftReview extends Review {
+    defaults(): DraftReviewAttrs {
+        return _.defaults({
+            publishAndArchive: false,
+            publishToOwnerOnly: false,
+        }, super.defaults());
+    }
 
-    attrToJsonMap: _.defaults({
+    static attrToJsonMap = _.defaults({
         publishAndArchive: 'publish_and_archive',
         publishToOwnerOnly: 'publish_to_owner_only',
-    }, RB.Review.prototype.attrToJsonMap),
+    }, Review.attrToJsonMap);
 
-    serializedAttrs: [
+    static serializedAttrs = [
         'publishAndArchive',
         'publishToOwnerOnly',
-    ].concat(RB.Review.prototype.serializedAttrs),
-
-    serializers: _.defaults({
-        publishAndArchive: RB.JSONSerializers.onlyIfValue,
-        publishToOwnerOnly: RB.JSONSerializers.onlyIfValue,
-    }, RB.Review.prototype.serializers),
+    ].concat(Review.serializedAttrs);
 
+    static serializers = _.defaults({
+        publishAndArchive: JSONSerializers.onlyIfValue,
+        publishToOwnerOnly: JSONSerializers.onlyIfValue,
+    }, Review.serializers);
 
     /**
      * Publish the review.
@@ -51,12 +87,16 @@ RB.DraftReview = RB.Review.extend(_.extend({
      *     Promise:
      *     A promise which resolves when the operation is complete.
      */
-    async publish(options={}, context=undefined) {
+    async publish(
+        options: SaveOptions = {},
+        context: object = undefined,
+    ) {
         if (_.isFunction(options.success) ||
             _.isFunction(options.error) ||
             _.isFunction(options.complete)) {
-            console.warn('RB.DraftReview.publish was called using callbacks. ' +
-                         'Callers should be updated to use promises instead.');
+            console.warn(`RB.DraftReview.publish was called using callbacks.
+                          Callers should be updated to use promises instead.`);
+
             return RB.promiseToCallbacks(
                 options, context, newOptions => this.publish(newOptions));
         }
@@ -76,4 +116,4 @@ RB.DraftReview = RB.Review.extend(_.extend({
 
         this.trigger('published');
     }
-}, RB.DraftResourceModelMixin));
+}
diff --git a/reviewboard/static/rb/js/resources/models/tests/draftReviewModelTests.es6.js b/reviewboard/static/rb/js/common/resources/models/tests/draftReviewModelTests.ts
similarity index 90%
rename from reviewboard/static/rb/js/resources/models/tests/draftReviewModelTests.es6.js
rename to reviewboard/static/rb/js/common/resources/models/tests/draftReviewModelTests.ts
index 6ad83f6b57c1755fc6e14dccc0e4f46ba95fe5f0..1d0657b3f690313a4e0df63322465278eb3c867e 100644
--- a/reviewboard/static/rb/js/resources/models/tests/draftReviewModelTests.es6.js
+++ b/reviewboard/static/rb/js/common/resources/models/tests/draftReviewModelTests.ts
@@ -1,17 +1,29 @@
+import {
+    beforeEach,
+    describe,
+    expect,
+    it,
+    spyOn,
+} from 'jasmine-core';
+
+import { BaseResource } from '../baseResourceModel';
+import { DraftReview } from '../draftReviewModel';
+
+
 suite('rb/resources/models/DraftReview', function() {
     let model;
     let parentObject;
 
     beforeEach(function() {
-        parentObject = new RB.BaseResource({
+        parentObject = new BaseResource({
             links: {
                 reviews: {
-                    href: '/api/foos/'
+                    href: '/api/foos/',
                 },
             },
         });
 
-        model = new RB.DraftReview({
+        model = new DraftReview({
             parentObject: parentObject,
         });
         model.rspNamespace = 'foo';
@@ -58,6 +70,7 @@ suite('rb/resources/models/DraftReview', function() {
                 spyOn(console, 'warn');
 
                 model.ready({
+                    error: () => done.fail(),
                     success: () => {
                         expect(parentObject.ready).toHaveBeenCalled();
                         expect(model._retrieveDraft).toHaveBeenCalled();
@@ -65,7 +78,6 @@ suite('rb/resources/models/DraftReview', function() {
 
                         done();
                     },
-                    error: () => done.fail(),
                 });
             });
         });
diff --git a/reviewboard/staticbundles.py b/reviewboard/staticbundles.py
index 360a5e2c9c2884124379afd8fba487bfe3aafaab..f8ece3e4eb3ac9a42bf6b249237bcb3e5c276b36 100644
--- a/reviewboard/staticbundles.py
+++ b/reviewboard/staticbundles.py
@@ -66,7 +66,6 @@ PIPELINE_JAVASCRIPT = {
             'rb/js/resources/models/tests/baseCommentReplyModelTests.es6.js',
             'rb/js/resources/models/tests/defaultReviewerModelTests.es6.js',
             'rb/js/resources/models/tests/diffCommentModelTests.es6.js',
-            'rb/js/resources/models/tests/draftReviewModelTests.es6.js',
             'rb/js/resources/models/tests/draftReviewRequestModelTests.es6.js',
             'rb/js/resources/models/tests/fileAttachmentModelTests.es6.js',
             'rb/js/resources/models/tests/fileAttachmentCommentModelTests.es6.js',
@@ -147,7 +146,6 @@ PIPELINE_JAVASCRIPT = {
             'rb/js/resources/models/repositoryCommitModel.es6.js',
             'rb/js/resources/models/draftResourceChildModelMixin.es6.js',
             'rb/js/resources/models/draftReviewRequestModel.es6.js',
-            'rb/js/resources/models/draftReviewModel.es6.js',
             'rb/js/resources/models/baseCommentModel.es6.js',
             'rb/js/resources/models/baseCommentReplyModel.es6.js',
             'rb/js/resources/models/defaultReviewerModel.es6.js',
