From 7409b6005ec529c04124d2a201c1f188c64ff48a Mon Sep 17 00:00:00 2001 From: Andrej Dyck <43913051+andrej-dyck@users.noreply.github.com> Date: Sat, 16 Dec 2023 14:44:28 +0100 Subject: [PATCH] add record extensions --- records/hasField.test.ts | 16 ++++++++++++++++ records/hasField.ts | 13 +++++++++++++ records/index.ts | 3 +++ records/omitFields.test.ts | 15 +++++++++++++++ records/omitFields.ts | 19 +++++++++++++++++++ records/pickFields.test.ts | 15 +++++++++++++++ records/pickFields.ts | 18 ++++++++++++++++++ 7 files changed, 99 insertions(+) create mode 100644 records/hasField.test.ts create mode 100644 records/hasField.ts create mode 100644 records/index.ts create mode 100644 records/omitFields.test.ts create mode 100644 records/omitFields.ts create mode 100644 records/pickFields.test.ts create mode 100644 records/pickFields.ts diff --git a/records/hasField.test.ts b/records/hasField.test.ts new file mode 100644 index 0000000..2bc65da --- /dev/null +++ b/records/hasField.test.ts @@ -0,0 +1,16 @@ +import { expect, test } from 'vitest' +import { hasField } from './hasField.js' + +test('hasField infers that a field exists on an unknown object', () => { + const obj: unknown = { id: 1 } + expect(hasField(obj, 'id') ? obj.id : 0).toBe(1) +}) + +test.each([ + { obj: { a: 1 }, key: 'a', expected: true }, + { obj: { a: 1 }, key: 'b', expected: false }, + { obj: {}, key: 'a', expected: false }, + { obj: undefined, key: 'a', expected: false }, +])('hasField checks that a field exists on an unknown object', ({ obj, key, expected }) => { + expect(hasField(obj, key)).toBe(expected) +}) diff --git a/records/hasField.ts b/records/hasField.ts new file mode 100644 index 0000000..8544d4b --- /dev/null +++ b/records/hasField.ts @@ -0,0 +1,13 @@ +/** + * Checks if the given object has the specified field. + */ +export const hasField =

(obj: unknown, key: P): obj is Record => + obj != null && typeof obj === 'object' && key in obj + +/* + * Example: + * const obj: { data: unknown } | { error: string } | unknown = ... + * hasField(obj, 'data') + * ? JSON.stringify(obj.data) // inferred that the data field exists + * : obj?.error // inferred that it's nullable or has error field + */ diff --git a/records/index.ts b/records/index.ts new file mode 100644 index 0000000..5002ef4 --- /dev/null +++ b/records/index.ts @@ -0,0 +1,3 @@ +export * from './hasField.js' +export * from './pickFields.js' +export * from './omitFields.js' diff --git a/records/omitFields.test.ts b/records/omitFields.test.ts new file mode 100644 index 0000000..869b01f --- /dev/null +++ b/records/omitFields.test.ts @@ -0,0 +1,15 @@ +import { expect, test } from 'vitest' +import { omitFields } from './omitFields.js' + +test('omitFields omits a field from an object', () => { + expect(omitFields({ a: 1, b: 2, c: 3 }, 'b')).toMatchObject({ a: 1, c: 3 }) +}) + +test('omitFields omits multiple fields from an object', () => { + expect(omitFields({ a: 1, b: 2, c: 3 }, 'b', 'c')).toMatchObject({ a: 1 }) +}) + +test('omitFields returns same obj if no keys are specified', () => { + const obj = { a: 1, b: 2, c: 3 } + expect(omitFields(obj)).toBe(obj) +}) diff --git a/records/omitFields.ts b/records/omitFields.ts new file mode 100644 index 0000000..cdeea54 --- /dev/null +++ b/records/omitFields.ts @@ -0,0 +1,19 @@ +/** + * Omits specified fields from an object and returns a copy without those fields. + */ +export const omitFields = , P extends keyof T>(obj: T, ...keys: P[]): Omit => { + if (keys.length === 0) return obj + + const copy = { ...obj } + for (const k of keys) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete copy[k] + } + return copy +} + +/* + * Example: + * const order = { id: ..., date: ..., customer: ..., status: ..., items: [...] } + * const result = omitFields(order, 'customer', 'status') // { id: ..., date: ..., items: [...] } + */ diff --git a/records/pickFields.test.ts b/records/pickFields.test.ts new file mode 100644 index 0000000..db5a0a4 --- /dev/null +++ b/records/pickFields.test.ts @@ -0,0 +1,15 @@ +import { expect, test } from 'vitest' +import { pickFields } from './pickFields.js' + +test('pickFields picks a field from an object', () => { + expect(pickFields({ a: 1, b: 2, c: 3 }, 'b')).toMatchObject({ b: 2 }) +}) + +test('pickFields picks multiple fields from an object', () => { + expect(pickFields({ a: 1, b: 2, c: 3 }, 'a', 'b')).toMatchObject({ a: 1, b: 2 }) +}) + +test('pickFields returns same obj if no keys are specified', () => { + const obj = { a: 1, b: 2, c: 3 } + expect(pickFields(obj)).toBe(obj) +}) diff --git a/records/pickFields.ts b/records/pickFields.ts new file mode 100644 index 0000000..0a88024 --- /dev/null +++ b/records/pickFields.ts @@ -0,0 +1,18 @@ +/** + * Picks specific fields from an object and returns a copy with only those fields. + */ +export const pickFields = , P extends keyof T>(obj: T, ...keys: P[]): Pick => { + if (keys.length === 0) return obj + + const copy = {} as Pick + for (const k of keys) { + copy[k] = obj[k] + } + return copy +} + +/* + * Example: + * const order = { id: ..., date: ..., customer: ..., status: ..., items: [...] } + * const result = pickFields(order, 'id', 'date') // { id: ..., date: ... } + */