@relational-fabric/canon 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +329 -0
- package/eslint.js +31 -0
- package/package.json +126 -0
- package/src/axiom.ts +34 -0
- package/src/axioms/id.ts +53 -0
- package/src/axioms/references.ts +98 -0
- package/src/axioms/timestamps.ts +77 -0
- package/src/axioms/type.ts +53 -0
- package/src/axioms/version.ts +53 -0
- package/src/canon.ts +47 -0
- package/src/index.ts +30 -0
- package/src/radar/converter.ts +124 -0
- package/src/radar/index.ts +11 -0
- package/src/radar/validator.ts +310 -0
- package/src/registry.test.ts +75 -0
- package/src/registry.ts +72 -0
- package/src/shell.test.ts +63 -0
- package/src/shell.ts +49 -0
- package/src/testing.ts +43 -0
- package/src/types/axioms.ts +131 -0
- package/src/types/canons.ts +78 -0
- package/src/types/guards.ts +51 -0
- package/src/types/index.ts +10 -0
- package/src/types/js.ts +25 -0
- package/src/types/objects.ts +32 -0
- package/src/types/radar.ts +106 -0
- package/src/utils/guards.test.ts +27 -0
- package/src/utils/guards.ts +29 -0
- package/src/utils/objects.test.ts +141 -0
- package/src/utils/objects.ts +172 -0
- package/tsconfig.base.json +11 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Technology Radar data structures and types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { type Expect, invariant } from '../testing.js'
|
|
6
|
+
|
|
7
|
+
export interface RadarEntry {
|
|
8
|
+
/** The name of the technology or practice */
|
|
9
|
+
name: string
|
|
10
|
+
/** Description of the technology or practice */
|
|
11
|
+
description: string
|
|
12
|
+
/** Whether this is a new entry */
|
|
13
|
+
isNew: boolean
|
|
14
|
+
/** Optional justification for the placement */
|
|
15
|
+
justification?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Quadrant {
|
|
19
|
+
/** Unique identifier for the quadrant */
|
|
20
|
+
id: string
|
|
21
|
+
/** Display name for the quadrant */
|
|
22
|
+
name: string
|
|
23
|
+
/** Description of what this quadrant represents */
|
|
24
|
+
description: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Ring {
|
|
28
|
+
/** Unique identifier for the ring */
|
|
29
|
+
id: string
|
|
30
|
+
/** Display name for the ring */
|
|
31
|
+
name: string
|
|
32
|
+
/** Description of what this ring represents */
|
|
33
|
+
description: string
|
|
34
|
+
/** Color code for the ring (hex format) */
|
|
35
|
+
color: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface RadarMetadata {
|
|
39
|
+
/** Title of the radar */
|
|
40
|
+
title: string
|
|
41
|
+
/** Subtitle of the radar */
|
|
42
|
+
subtitle: string
|
|
43
|
+
/** Version of the radar data */
|
|
44
|
+
version: string
|
|
45
|
+
/** Last updated date */
|
|
46
|
+
lastUpdated: string
|
|
47
|
+
/** Optional description */
|
|
48
|
+
description?: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface RadarData {
|
|
52
|
+
/** Metadata about the radar */
|
|
53
|
+
metadata: RadarMetadata
|
|
54
|
+
/** Quadrant definitions */
|
|
55
|
+
quadrants: Quadrant[]
|
|
56
|
+
/** Ring definitions */
|
|
57
|
+
rings: Ring[]
|
|
58
|
+
/** Radar entries organized by quadrant and ring */
|
|
59
|
+
entries: Record<string, Record<string, RadarEntry[]>>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface RadarConfig {
|
|
63
|
+
/** Configuration for the radar */
|
|
64
|
+
metadata: RadarMetadata
|
|
65
|
+
/** Quadrant configuration */
|
|
66
|
+
quadrants: Quadrant[]
|
|
67
|
+
/** Ring configuration */
|
|
68
|
+
rings: Ring[]
|
|
69
|
+
/** Build configuration */
|
|
70
|
+
buildConfig: {
|
|
71
|
+
csvOutput: string
|
|
72
|
+
yamlInput: string
|
|
73
|
+
includeMetadata: boolean
|
|
74
|
+
sortByRing: boolean
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface CsvRow {
|
|
79
|
+
name: string
|
|
80
|
+
ring: string
|
|
81
|
+
quadrant: string
|
|
82
|
+
isNew: boolean
|
|
83
|
+
description: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type QuadrantKey =
|
|
87
|
+
| 'tools-libraries'
|
|
88
|
+
| 'techniques-patterns'
|
|
89
|
+
| 'features-capabilities'
|
|
90
|
+
| 'data-structures-formats'
|
|
91
|
+
export type RingKey = 'adopt' | 'trial' | 'assess' | 'hold'
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Compile-time invariants
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
void invariant<Expect<RadarEntry['isNew'], boolean>>()
|
|
98
|
+
void invariant<Expect<Quadrant['id'], string>>()
|
|
99
|
+
void invariant<Expect<Ring['color'], string>>()
|
|
100
|
+
void invariant<
|
|
101
|
+
Expect<
|
|
102
|
+
QuadrantKey,
|
|
103
|
+
'tools-libraries' | 'techniques-patterns' | 'features-capabilities' | 'data-structures-formats'
|
|
104
|
+
>
|
|
105
|
+
>()
|
|
106
|
+
void invariant<Expect<RingKey, 'adopt' | 'trial' | 'assess' | 'hold'>>()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for type guard utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from 'vitest'
|
|
6
|
+
import { typeGuard } from './guards.js'
|
|
7
|
+
|
|
8
|
+
describe('typeGuard', () => {
|
|
9
|
+
it('should convert predicate to TypeGuard', () => {
|
|
10
|
+
const isString = typeGuard<string>((v: unknown) => typeof v === 'string')
|
|
11
|
+
|
|
12
|
+
expect(isString('hello')).toBe(true)
|
|
13
|
+
expect(isString(123)).toBe(false)
|
|
14
|
+
expect(isString(null)).toBe(false)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('should work with complex predicates', () => {
|
|
18
|
+
const hasId = typeGuard<{ id: string }>(
|
|
19
|
+
(v: unknown) =>
|
|
20
|
+
typeof v === 'object' && v !== null && 'id' in v && typeof (v as any).id === 'string',
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
expect(hasId({ id: '123' })).toBe(true)
|
|
24
|
+
expect(hasId({ name: 'test' })).toBe(false)
|
|
25
|
+
expect(hasId(null)).toBe(false)
|
|
26
|
+
})
|
|
27
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type guard utility functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Predicate, TypeGuard } from '../types/index.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Convert a predicate function to a proper TypeGuard
|
|
9
|
+
*
|
|
10
|
+
* Accepts predicates (boolean-returning functions) and converts them to
|
|
11
|
+
* TypeGuards with proper overload signatures that discriminate types correctly.
|
|
12
|
+
*
|
|
13
|
+
* @param predicate - A boolean-returning predicate function
|
|
14
|
+
* @returns A TypeGuard with proper type discrimination
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* // Convert a simple predicate to a type guard
|
|
19
|
+
* const isString = typeGuard<string>(v => typeof v === 'string')
|
|
20
|
+
*
|
|
21
|
+
* // Use with object checks
|
|
22
|
+
* const hasId = typeGuard<{ id: string }>(v =>
|
|
23
|
+
* isPojo(v) && 'id' in v && typeof v.id === 'string'
|
|
24
|
+
* )
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function typeGuard<T>(predicate: Predicate<T>): TypeGuard<T> {
|
|
28
|
+
return predicate as TypeGuard<T>
|
|
29
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for object utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from 'vitest'
|
|
6
|
+
import {
|
|
7
|
+
isPojo,
|
|
8
|
+
objectEntries,
|
|
9
|
+
objectKeys,
|
|
10
|
+
objectValues,
|
|
11
|
+
pojoHas,
|
|
12
|
+
pojoHasOfType,
|
|
13
|
+
pojoWithOfType,
|
|
14
|
+
} from './objects.js'
|
|
15
|
+
|
|
16
|
+
describe('isPojo', () => {
|
|
17
|
+
it('should return true for plain objects', () => {
|
|
18
|
+
expect(isPojo({})).toBe(true)
|
|
19
|
+
expect(isPojo({ id: '123' })).toBe(true)
|
|
20
|
+
expect(isPojo({ nested: { value: 1 } })).toBe(true)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should return false for arrays', () => {
|
|
24
|
+
expect(isPojo([])).toBe(false)
|
|
25
|
+
expect(isPojo([1, 2, 3])).toBe(false)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should return false for null', () => {
|
|
29
|
+
expect(isPojo(null)).toBe(false)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should return false for primitives', () => {
|
|
33
|
+
expect(isPojo('string')).toBe(false)
|
|
34
|
+
expect(isPojo(123)).toBe(false)
|
|
35
|
+
expect(isPojo(true)).toBe(false)
|
|
36
|
+
expect(isPojo(undefined)).toBe(false)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('should return false for class instances', () => {
|
|
40
|
+
class MyClass {}
|
|
41
|
+
expect(isPojo(new MyClass())).toBe(false)
|
|
42
|
+
expect(isPojo(new Date())).toBe(false)
|
|
43
|
+
expect(isPojo(new Map())).toBe(false)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should return false for objects with custom prototypes', () => {
|
|
47
|
+
const customProto = Object.create({ custom: true })
|
|
48
|
+
expect(isPojo(customProto)).toBe(false)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('pojoHas', () => {
|
|
53
|
+
it('should return true when property exists', () => {
|
|
54
|
+
const obj = { id: '123', name: 'Test' }
|
|
55
|
+
expect(pojoHas(obj, 'id')).toBe(true)
|
|
56
|
+
expect(pojoHas(obj, 'name')).toBe(true)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should return false when property does not exist', () => {
|
|
60
|
+
const obj = { id: '123' }
|
|
61
|
+
expect(pojoHas(obj, 'missing')).toBe(false)
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('pojoHasOfType', () => {
|
|
66
|
+
it('should return true for object with string field', () => {
|
|
67
|
+
expect(pojoHasOfType({ id: '123' }, 'id', 'string')).toBe(true)
|
|
68
|
+
expect(pojoHasOfType({ '@id': 'uri' }, '@id', 'string')).toBe(true)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should return false for non-string field when checking for string', () => {
|
|
72
|
+
expect(pojoHasOfType({ id: 123 }, 'id', 'string')).toBe(false)
|
|
73
|
+
expect(pojoHasOfType({ id: null }, 'id', 'string')).toBe(false)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('should return false for missing field', () => {
|
|
77
|
+
expect(pojoHasOfType({ name: 'test' }, 'id', 'string')).toBe(false)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('should return false for non-objects', () => {
|
|
81
|
+
expect(pojoHasOfType('string', 'id', 'string')).toBe(false)
|
|
82
|
+
expect(pojoHasOfType(null, 'id', 'string')).toBe(false)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('should work with number type', () => {
|
|
86
|
+
expect(pojoHasOfType({ count: 123 }, 'count', 'number')).toBe(true)
|
|
87
|
+
expect(pojoHasOfType({ count: '123' }, 'count', 'number')).toBe(false)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('should work with boolean type', () => {
|
|
91
|
+
expect(pojoHasOfType({ active: true }, 'active', 'boolean')).toBe(true)
|
|
92
|
+
expect(pojoHasOfType({ active: 'true' }, 'active', 'boolean')).toBe(false)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('should work with object type', () => {
|
|
96
|
+
expect(pojoHasOfType({ meta: { nested: true } }, 'meta', 'object')).toBe(true)
|
|
97
|
+
expect(pojoHasOfType({ meta: 'string' }, 'meta', 'object')).toBe(false)
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe('pojoWithOfType', () => {
|
|
102
|
+
it('should create reusable type guard for string fields', () => {
|
|
103
|
+
const hasStringId = pojoWithOfType('id', 'string')
|
|
104
|
+
expect(hasStringId({ id: '123' })).toBe(true)
|
|
105
|
+
expect(hasStringId({ id: 123 })).toBe(false)
|
|
106
|
+
expect(hasStringId({ name: 'test' })).toBe(false)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('should create reusable type guard for number fields', () => {
|
|
110
|
+
const hasNumberCount = pojoWithOfType('count', 'number')
|
|
111
|
+
expect(hasNumberCount({ count: 42 })).toBe(true)
|
|
112
|
+
expect(hasNumberCount({ count: '42' })).toBe(false)
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
describe('objectKeys', () => {
|
|
117
|
+
it('should return typed keys', () => {
|
|
118
|
+
const obj = { a: 1, b: 2, c: 3 }
|
|
119
|
+
const keys = objectKeys(obj)
|
|
120
|
+
expect(keys).toEqual(['a', 'b', 'c'])
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
describe('objectValues', () => {
|
|
125
|
+
it('should return values', () => {
|
|
126
|
+
const obj = { a: 1, b: 2, c: 3 }
|
|
127
|
+
const values = objectValues(obj)
|
|
128
|
+
expect(values).toEqual([1, 2, 3])
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
describe('objectEntries', () => {
|
|
133
|
+
it('should return entries', () => {
|
|
134
|
+
const obj = { a: 1, b: 2 }
|
|
135
|
+
const entries = objectEntries(obj)
|
|
136
|
+
expect(entries).toEqual([
|
|
137
|
+
['a', 1],
|
|
138
|
+
['b', 2],
|
|
139
|
+
])
|
|
140
|
+
})
|
|
141
|
+
})
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Object utilities for Canon
|
|
3
|
+
*
|
|
4
|
+
* Helpers for working with plain JavaScript objects.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { JsType, JsTypeName, Pojo, PojoWith, TypeGuard } from '../types/index.js'
|
|
8
|
+
import { typeGuard } from './guards.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Re-export types for convenience
|
|
12
|
+
*/
|
|
13
|
+
export type { Pojo, PojoWith }
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Type guard to check if a value is a plain JavaScript object
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* isPojo({ id: '123' }) // true
|
|
21
|
+
* isPojo([1, 2, 3]) // false
|
|
22
|
+
* isPojo(null) // false
|
|
23
|
+
* isPojo('string') // false
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export const isPojo = typeGuard<Pojo>(
|
|
27
|
+
(value: unknown) =>
|
|
28
|
+
typeof value === 'object'
|
|
29
|
+
&& value !== null
|
|
30
|
+
&& !Array.isArray(value)
|
|
31
|
+
&& Object.getPrototypeOf(value) === Object.prototype,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create a type guard for Pojo with a specific property
|
|
36
|
+
*
|
|
37
|
+
* Higher-order function that returns a reusable TypeGuard.
|
|
38
|
+
*
|
|
39
|
+
* @param key - The property name to check for
|
|
40
|
+
* @returns TypeGuard that checks if value is a Pojo with that property
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* const hasId = pojoWith('id')
|
|
45
|
+
* hasId({ id: '123' }) // true
|
|
46
|
+
* hasId({ name: 'test' }) // false
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export function pojoWith<K extends string>(key: K): TypeGuard<PojoWith<Pojo, K, unknown>> {
|
|
50
|
+
return typeGuard((value: unknown) => isPojo(value) && key in value)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Convenience function to check if a Pojo has a specific property
|
|
55
|
+
*
|
|
56
|
+
* Uses pojoWith() internally.
|
|
57
|
+
*
|
|
58
|
+
* @param obj - The object to check
|
|
59
|
+
* @param key - The property name to look for
|
|
60
|
+
* @returns True if the object has the property
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* const data = { id: '123', name: 'Test' }
|
|
65
|
+
* pojoHas(data, 'id') // true
|
|
66
|
+
* pojoHas(data, 'missing') // false
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export function pojoHas<T extends Pojo, K extends string>(
|
|
70
|
+
obj: T | unknown,
|
|
71
|
+
key: K,
|
|
72
|
+
): obj is PojoWith<T, K, unknown> {
|
|
73
|
+
return pojoWith(key)(obj)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function pojoWithOfType<K extends string, V extends JsTypeName>(
|
|
77
|
+
key: K,
|
|
78
|
+
type: V,
|
|
79
|
+
): TypeGuard<PojoWith<Pojo, K, JsType[V]>> {
|
|
80
|
+
return typeGuard((value: unknown) => {
|
|
81
|
+
if (!pojoHas(value, key)) {
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
const valueType: JsTypeName = typeof value[key] // Needs to be seperate because we're not using a string literal
|
|
85
|
+
return valueType === type
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Convenience function to check if a value has a specific string field
|
|
91
|
+
*
|
|
92
|
+
* @param value - The value to check
|
|
93
|
+
* @param key - The field name that should contain a string
|
|
94
|
+
* @returns True if value is a Pojo with the specified string field
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```typescript
|
|
98
|
+
* pojoHasString({ id: '123' }, 'id') // true
|
|
99
|
+
* pojoHasString({ id: 123 }, 'id') // false
|
|
100
|
+
* pojoHasString('not object', 'id') // false
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export function pojoHasOfType<T extends Pojo, K extends string, V extends JsTypeName>(
|
|
104
|
+
value: T | unknown,
|
|
105
|
+
key: K,
|
|
106
|
+
type: V,
|
|
107
|
+
): value is PojoWith<T, K, JsType[V]> {
|
|
108
|
+
return pojoWithOfType(key, type)(value)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Type-safe Object.keys that handles both objects and arrays correctly
|
|
113
|
+
*
|
|
114
|
+
* For arrays, returns numeric indices. For objects, returns property keys.
|
|
115
|
+
*
|
|
116
|
+
* @param obj - The object to get keys from
|
|
117
|
+
* @returns Array of object keys
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```typescript
|
|
121
|
+
* objectKeys({ a: 1, b: 2 }) // ['a', 'b']
|
|
122
|
+
* objectKeys([1, 2, 3]) // [0, 1, 2]
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
export function objectKeys<T extends object>(obj: T): Array<keyof T> {
|
|
126
|
+
if (Array.isArray(obj)) {
|
|
127
|
+
return Array.from(obj.keys()) as Array<keyof T>
|
|
128
|
+
}
|
|
129
|
+
return Object.keys(obj) as Array<keyof T>
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Type-safe Object.values that handles both objects and arrays correctly
|
|
134
|
+
*
|
|
135
|
+
* For arrays, returns array values. For objects, returns property values.
|
|
136
|
+
*
|
|
137
|
+
* @param obj - The object to get values from
|
|
138
|
+
* @returns Array of object values
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```typescript
|
|
142
|
+
* objectValues({ a: 1, b: 2 }) // [1, 2]
|
|
143
|
+
* objectValues([1, 2, 3]) // [1, 2, 3]
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
export function objectValues<T extends object>(obj: T): unknown[] {
|
|
147
|
+
if (Array.isArray(obj)) {
|
|
148
|
+
return Array.from(obj)
|
|
149
|
+
}
|
|
150
|
+
return Object.values(obj)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Type-safe Object.entries that handles both objects and arrays correctly
|
|
155
|
+
*
|
|
156
|
+
* For arrays, returns [index, value] pairs. For objects, returns [key, value] pairs.
|
|
157
|
+
*
|
|
158
|
+
* @param obj - The object to get entries from
|
|
159
|
+
* @returns Array of [key, value] tuples
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* objectEntries({ a: 1, b: 2 }) // [['a', 1], ['b', 2]]
|
|
164
|
+
* objectEntries([1, 2, 3]) // [[0, 1], [1, 2], [2, 3]]
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
export function objectEntries<T extends object>(obj: T): Array<[keyof T, T[keyof T]]> {
|
|
168
|
+
if (Array.isArray(obj)) {
|
|
169
|
+
return Array.from(obj.entries()) as Array<[keyof T, T[keyof T]]>
|
|
170
|
+
}
|
|
171
|
+
return Object.entries(obj) as Array<[keyof T, T[keyof T]]>
|
|
172
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@tsconfig/node-lts/tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"resolveJsonModule": true,
|
|
5
|
+
"strict": true,
|
|
6
|
+
"allowSyntheticDefaultImports": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"forceConsistentCasingInFileNames": true,
|
|
9
|
+
"skipLibCheck": true
|
|
10
|
+
}
|
|
11
|
+
}
|