Skip to content

Commit 5617f28

Browse files
parzhitskybigmontz
authored andcommitted
Add type-safety to Record class
This commit adds ability to create type-safe Record entities by assigning type to Record's value. The implementation is based on adding type parameters to types, interfaces, and classes. It is implemented however in a backwards-compatible manner, so that opting in to type safety is optional. Examples: // before (unsafe) const record = new Record(['name'], ['Alice']) record.get('neam') // no errors record.toObject().neam[0] // run-time (late) error // after (safe) const record = new Record<Person>(['name'], ['Alice']) record.get('neam') // Error: does not exist record.toObject().neam[0] // design-time (early) error
1 parent 2cf69fe commit 5617f28

File tree

2 files changed

+62
-16
lines changed

2 files changed

+62
-16
lines changed

test/types/record.test.ts

+34-5
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,25 @@
1919

2020
import Record from '../../types/record'
2121

22+
interface Person {
23+
name: string
24+
age: number
25+
}
26+
2227
const record1 = new Record(['name', 'age'], ['Alice', 20])
23-
const record2 = new Record(['name', 'age'], ['Bob', 22], { key: 'value' })
28+
const record2 = new Record(['name', 'age'], ['Bob', 22], { firstName: 0 })
29+
const record3 = new Record<Person>(['name', 'age'], ['Carl', 24])
30+
2431
const isRecord1: boolean = record1 instanceof Record
2532
const isRecord2: boolean = record2 instanceof Record
33+
const isRecord3: boolean = record3 instanceof Record
2634

2735
const record1Keys: string[] = record1.keys
36+
const record3Keys: Array<keyof Person> = record3.keys
2837
const record1Length: number = record1.length
2938

3039
const record1Object: object = record1.toObject()
40+
const record3Object: Person = record3.toObject()
3141

3242
record1.forEach(() => {})
3343

@@ -37,6 +47,16 @@ record1.forEach((value: any, key: string) => {})
3747

3848
record1.forEach((value: any, key: string, record: Record) => {})
3949

50+
record3.forEach(
51+
(value: string | number, key: 'name' | 'age', record: Record<Person>) => {}
52+
)
53+
54+
const record3Mapped: [
55+
string | number,
56+
'name' | 'age',
57+
Record<Person>
58+
][] = record3.map((...args) => args)
59+
4060
const record1Entries: IterableIterator<[string, any]> = record1.entries()
4161
const record2Entries: IterableIterator<[string, any]> = record2.entries()
4262

@@ -49,8 +69,17 @@ const record2ToArray: any[] = [...record2]
4969
const record1Has: boolean = record1.has(42)
5070
const record2Has: boolean = record1.has('key')
5171

52-
const record1Get1: any = record1.get(42)
53-
const record2Get1: any = record2.get('key')
72+
const record1Get1: any = record1.get('name')
73+
const record2Get1: any = record2.get('age')
74+
75+
const record1Get2: object = record1.get('name')
76+
const record2Get2: string[] = record2.get('age')
77+
78+
const record3Get1: string = record3.get('name')
79+
const record3Get2: number = record3.get('age')
80+
81+
const record2Get3: string = record2.get('firstName')
82+
const record2Get4: number = record2.get(1)
5483

55-
const record1Get2: object = record1.get(42)
56-
const record2Get2: string[] = record2.get('key')
84+
// @ts-expect-error
85+
const record2Get5: any = record2.get('does-not-exist')

types/record.d.ts

+28-11
Original file line numberDiff line numberDiff line change
@@ -17,35 +17,52 @@
1717
* limitations under the License.
1818
*/
1919

20-
declare type Visitor = (value: any, key: string, record: Record) => void
20+
declare type Dict<Key extends PropertyKey = PropertyKey, Value = any> = {
21+
[K in Key]: Value
22+
}
23+
24+
declare type Visitor<
25+
Entries extends Dict = Dict,
26+
Key extends keyof Entries = keyof Entries
27+
> = MapVisitor<void, Entries, Key>
2128

22-
declare type MapVisitor<T> = (value: any, key: string, record: Record) => T
29+
declare type MapVisitor<
30+
ReturnType,
31+
Entries extends Dict = Dict,
32+
Key extends keyof Entries = keyof Entries
33+
> = (value: Entries[Key], key: Key, record: Record<Entries>) => ReturnType
2334

24-
declare class Record {
25-
keys: string[]
35+
declare class Record<
36+
Entries extends Dict = Dict,
37+
Key extends keyof Entries = keyof Entries,
38+
FieldLookup extends Dict<string, number> = Dict<string, number>
39+
> {
40+
keys: Key[]
2641
length: number
2742

2843
constructor(
29-
keys: string[],
44+
keys: Key[],
3045
fields: any[],
31-
fieldLookup?: { [index: string]: string }
46+
fieldLookup?: FieldLookup
3247
)
3348

34-
forEach(visitor: Visitor): void
49+
forEach(visitor: Visitor<Entries, Key>): void
3550

36-
map<T>(visitor: MapVisitor<T>): T[]
51+
map<Value>(visitor: MapVisitor<Value, Entries, Key>): Value[]
3752

3853
entries(): IterableIterator<[string, Object]>
3954

4055
values(): IterableIterator<Object>
4156

4257
[Symbol.iterator](): IterableIterator<Object>
4358

44-
toObject(): object
59+
toObject(): Entries
60+
61+
get<K extends Key>(key: K): Entries[K]
4562

46-
get(key: string | number): any
63+
get(key: keyof FieldLookup | number): any
4764

48-
has(key: string | number): boolean
65+
has(key: any): key is Key
4966
}
5067

5168
export default Record

0 commit comments

Comments
 (0)