diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0abf683..96f10b7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,8 @@
- replaced by `nonStrict`: `sloppyRecord(X)` -> `nonStrict(record(X))`
- fixes [#23](https://github.com/hoeck/simple-runtypes/issues/23)
- fix: allow literal booleans as type-discriminators in discriminated unions, see [#98](https://github.com/hoeck/simple-runtypes/issues/98)
+- fix: `string` trim & maxlength checks: https://github.com/hoeck/simple-runtypes/issues/100
+- fix: `record` optionals: https://github.com/hoeck/simple-runtypes/issues/100
### 7.1.3
diff --git a/src/optional.ts b/src/optional.ts
index 19d7a68..d2e50f9 100644
--- a/src/optional.ts
+++ b/src/optional.ts
@@ -25,6 +25,6 @@ export function optional(t: Runtype): OptionalRuntype {
return ti(v, failOrThrow)
},
- { isPure: ti.meta?.isPure },
+ { isPure: ti.meta?.isPure, optional: true },
) as OptionalRuntype
}
diff --git a/src/record.ts b/src/record.ts
index f3cb149..3cf89ca 100644
--- a/src/record.ts
+++ b/src/record.ts
@@ -54,6 +54,14 @@ export function internalRecord(
const k = typemapKeys[i]
const t = typemapValues[i] as InternalRuntype
+ // optional fields not present in the given object do not need to be
+ // checked at all
+ // this is vital to preserve the object shape of an impure record
+ // with optional fields
+ if (t.meta?.optional && !o.hasOwnProperty(k)) {
+ break
+ }
+
// nested types should always fail with explicit `Fail` so we can add additional data
const value = t(o[k], failSymbol)
diff --git a/src/runtype.ts b/src/runtype.ts
index efcb626..789fe7a 100644
--- a/src/runtype.ts
+++ b/src/runtype.ts
@@ -184,6 +184,9 @@ export interface RuntypeMetadata {
// literal value used to identify tagged union members
literal?: string | boolean | number
+
+ // when true, this runtype is an optional in a record
+ optional?: boolean
}
/**
diff --git a/test/helpers.ts b/test/helpers.ts
index 0728ae1..90fa451 100644
--- a/test/helpers.ts
+++ b/test/helpers.ts
@@ -16,7 +16,8 @@ export function expectAcceptValuesImpure(
const [vIn, vOut] = valuesAreTuples ? (v as [unknown, unknown]) : [v, v]
const result = st.use(rt, vIn)
- expect(result).toEqual({ ok: true, result: vOut })
+ expect(result).toStrictEqual({ ok: true, result: vOut })
+
// this identity check does not account for unmodified primitive values,
// which are always identical to themselves; these cases are handled
// directly in the relevant tests
@@ -39,7 +40,7 @@ export function expectAcceptValuesPure(
values.forEach((v) => {
const result = st.use(rt, v)
- expect(result).toEqual({ ok: true, result: v })
+ expect(result).toStrictEqual({ ok: true, result: v })
expect(result.ok && result.result).toBe(v)
expect(() => rt(v)).not.toThrow()
diff --git a/test/issues.test.ts b/test/issues.test.ts
new file mode 100644
index 0000000..36eb612
--- /dev/null
+++ b/test/issues.test.ts
@@ -0,0 +1,30 @@
+import { st } from './helpers'
+
+describe('github issues', () => {
+ it('#100 (1)', () => {
+ expect(
+ st.use(
+ st.partial(
+ st.record({ name: st.string({ trim: false }), other: st.string() }),
+ ),
+ {},
+ ),
+ ).toEqual({ ok: true, result: {} })
+
+ expect(
+ st.use(
+ st.partial(
+ st.record({ name: st.string({ trim: true }), other: st.string() }),
+ ),
+ {},
+ ),
+ ).toEqual({ ok: true, result: {} })
+ })
+
+ it('#100 (2)', () => {
+ const s = st.string({ trim: true, minLength: 1, maxLength: 3 })
+
+ expect(() => s(' ')).toThrow()
+ expect(s('abc ')).toEqual('abc')
+ })
+})
diff --git a/test/record.test.ts b/test/record.test.ts
index 20326e1..acfbd94 100644
--- a/test/record.test.ts
+++ b/test/record.test.ts
@@ -56,6 +56,21 @@ describe('record', () => {
])
})
+ it('keeps optional attributes of impure records', () => {
+ const runtype = st.record({ a: st.optional(st.string({ trim: true })) })
+
+ expectAcceptValuesImpure(
+ runtype,
+ [
+ [{}, {}],
+ [{ a: '' }, { a: '' }],
+ [{ a: ' a ' }, { a: 'a' }],
+ [{ a: undefined }, { a: undefined }],
+ ],
+ true,
+ )
+ })
+
it('accepts nested records', () => {
const runtype = st.record({
a: st.record({