diff --git a/src/staged-query/schema.ts b/src/staged-query/schema.ts
index 6e06d7a..d9abc26 100644
--- a/src/staged-query/schema.ts
+++ b/src/staged-query/schema.ts
@@ -49,7 +49,14 @@ interface IoTsValidation {
readonly _tag: "Left" | "Right";
}
-interface IoTsCodec {
+/**
+ * The shape of an io-ts codec that a schema field accepts
+ *
+ * Declared as a type alias so TypeScript gives it an implicit string index
+ * signature: `Object.entries` on a codec lists its own enumerable properties
+ * as `[string, unknown]`, where an interface would make them `any`.
+ */
+type IoTsCodec = {
readonly _A: A;
readonly _O: O;
readonly _I: I;
@@ -57,7 +64,7 @@ interface IoTsCodec {
is(u: A | I): boolean;
encode(a: A): O;
decode(i: I): IoTsValidation;
-}
+};
export type FieldType = StringFieldType | IoTsCodec;
diff --git a/src/tests/field_type.test.ts b/src/tests/field_type.test.ts
new file mode 100644
index 0000000..b0aa796
--- /dev/null
+++ b/src/tests/field_type.test.ts
@@ -0,0 +1,34 @@
+import { expect } from "chai";
+import type { FieldType, StringFieldType } from "../schema";
+import type { Same } from "./helpers/same-type";
+
+type IsAny = 0 extends 1 & T ? true : false;
+
+type Codec = Exclude;
+
+function propertiesOf(codec: Codec) {
+ return Object.entries(codec);
+}
+
+type PropertyValue = ReturnType[number][1];
+
+const valuesAreNotAny: Same, false> = true;
+const valuesAreUnknown: Same = true;
+
+describe("Field types", () => {
+ it("Types the own properties of a codec as unknown values", () => {
+ const codec: Codec = {
+ _A: 0,
+ _O: 0,
+ _I: 0,
+ name: "number",
+ is: (u): boolean => u === 0,
+ encode: (a) => a,
+ decode: () => ({ _tag: "Right" }),
+ };
+ const names = propertiesOf(codec).map(([name]) => name);
+ expect(names).to.include.members(["name", "is", "encode", "decode"]);
+ expect(valuesAreNotAny).to.equal(true);
+ expect(valuesAreUnknown).to.equal(true);
+ });
+});