diff --git a/docs/modules/Semigroup.ts.md b/docs/modules/Semigroup.ts.md
index 787da91ffb..55f7f67a9a 100644
--- a/docs/modules/Semigroup.ts.md
+++ b/docs/modules/Semigroup.ts.md
@@ -61,6 +61,7 @@ Added in v2.0.0
- [concatAll](#concatall)
- [intercalate](#intercalate)
- [reverse](#reverse)
+ - [partial](#partial)
- [struct](#struct)
- [tuple](#tuple)
- [zone of death](#zone-of-death)
@@ -246,6 +247,39 @@ const S1 = pipe(S.Semigroup, intercalate(' + '))
assert.strictEqual(S1.concat('a', 'b'), 'a + b')
```
+Added in v2.14.0
+
+## partial
+
+Given a struct of semigroups returns a semigroup for the struct, that can have optional keys.
+
+**Signature**
+
+```ts
+export declare const partial: (
+ semigroups: { [K in keyof A]: Semigroup }
+) => Semigroup<{ readonly [K in keyof Partial]: A[K] }>
+```
+
+**Example**
+
+```ts
+import { partial, last } from 'fp-ts/Semigroup'
+import * as S from 'fp-ts/string'
+
+interface Person {
+ readonly name: string
+ readonly age: number
+}
+
+const S1 = partial({
+ name: S.Semigroup,
+ age: last(),
+})
+
+assert.deepStrictEqual(S1.concat({ name: 'first', age: 42 }, { name: 'second' }), { name: 'firstsecond', age: 42 })
+```
+
Added in v2.10.0
## reverse
diff --git a/src/Semigroup.ts b/src/Semigroup.ts
index 9df4f7f586..134a80e2ee 100644
--- a/src/Semigroup.ts
+++ b/src/Semigroup.ts
@@ -156,6 +156,46 @@ export const struct = (semigroups: { [K in keyof A]: Semigroup }): Semi
}
})
+/**
+ * Given a struct of semigroups returns a semigroup for the struct, that can have optional keys.
+ *
+ * @example
+ * import { partial, last } from 'fp-ts/Semigroup'
+ * import * as S from 'fp-ts/string'
+ *
+ * interface Person {
+ * readonly name: string
+ * readonly age: number
+ * }
+ *
+ * const S1 = partial({
+ * name: S.Semigroup,
+ * age: last()
+ * })
+ *
+ * assert.deepStrictEqual(S1.concat({ name: "first", age: 42 }, { name: "second" }), { name: "firstsecond", age: 42 })
+ *
+ * @category combinators
+ * @since 2.10.0
+ */
+export const partial = (
+ semigroups: { [K in keyof A]: Semigroup }
+): Semigroup<{ readonly [K in keyof Partial]: A[K] }> => ({
+ concat: (first, second) => {
+ const r: A = {} as any
+ for (const k in semigroups) {
+ if (_.has.call(semigroups, k)) {
+ if (_.has.call(first, k) && _.has.call(second, k)) {
+ r[k] = semigroups[k].concat(first[k], second[k])
+ } else if (_.has.call(first, k) || _.has.call(second, k)) {
+ r[k] = second[k] || first[k]
+ }
+ }
+ }
+ return r
+ }
+})
+
/**
* Given a tuple of semigroups returns a semigroup for the tuple.
*
diff --git a/test/Semigroup.ts b/test/Semigroup.ts
index ca98eb8c4e..fc07ca25e5 100644
--- a/test/Semigroup.ts
+++ b/test/Semigroup.ts
@@ -54,6 +54,15 @@ describe('Semigroup', () => {
U.deepStrictEqual(S.concat({}, {}), {})
})
+ it('partial', () => {
+ // should ignore non own properties
+ const S1 = _.partial(Object.create({ a: 1 }))
+ U.deepStrictEqual(S1.concat({}, {}), {})
+
+ const S2 = _.partial({ a: S.Semigroup, b: N.SemigroupSum, c: B.SemigroupAll, d: S.Semigroup })
+ U.deepStrictEqual(S2.concat({ a: 'first', b: 12 }, { b: 2, c: true }), { a: 'first', b: 14, c: true })
+ })
+
it('semigroupAll', () => {
const S = _.semigroupAll
U.deepStrictEqual(S.concat(true, true), true)