From 920251539a1d4a5422220a84f75862cc89754a09 Mon Sep 17 00:00:00 2001 From: Dmytro Parzhytskyi Date: Fri, 4 Dec 2020 17:40:56 +0200 Subject: [PATCH] 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(['name'], ['Alice']) record.get('neam') // Error: does not exist record.toObject().neam[0] // design-time (early) error --- test/types/record.test.ts | 39 ++++++++++++++++++++++++++++++++++----- types/record.d.ts | 39 ++++++++++++++++++++++++++++----------- 2 files changed, 62 insertions(+), 16 deletions(-) diff --git a/test/types/record.test.ts b/test/types/record.test.ts index 15c7c36ff..51f2f4645 100644 --- a/test/types/record.test.ts +++ b/test/types/record.test.ts @@ -19,15 +19,25 @@ import Record from '../../types/record' +interface Person { + name: string + age: number +} + const record1 = new Record(['name', 'age'], ['Alice', 20]) -const record2 = new Record(['name', 'age'], ['Bob', 22], { key: 'value' }) +const record2 = new Record(['name', 'age'], ['Bob', 22], { firstName: 0 }) +const record3 = new Record(['name', 'age'], ['Carl', 24]) + const isRecord1: boolean = record1 instanceof Record const isRecord2: boolean = record2 instanceof Record +const isRecord3: boolean = record3 instanceof Record const record1Keys: string[] = record1.keys +const record3Keys: Array = record3.keys const record1Length: number = record1.length const record1Object: object = record1.toObject() +const record3Object: Person = record3.toObject() record1.forEach(() => {}) @@ -37,6 +47,16 @@ record1.forEach((value: any, key: string) => {}) record1.forEach((value: any, key: string, record: Record) => {}) +record3.forEach( + (value: string | number, key: 'name' | 'age', record: Record) => {} +) + +const record3Mapped: [ + string | number, + 'name' | 'age', + Record +][] = record3.map((...args) => args) + const record1Entries: IterableIterator<[string, any]> = record1.entries() const record2Entries: IterableIterator<[string, any]> = record2.entries() @@ -49,8 +69,17 @@ const record2ToArray: any[] = [...record2] const record1Has: boolean = record1.has(42) const record2Has: boolean = record1.has('key') -const record1Get1: any = record1.get(42) -const record2Get1: any = record2.get('key') +const record1Get1: any = record1.get('name') +const record2Get1: any = record2.get('age') + +const record1Get2: object = record1.get('name') +const record2Get2: string[] = record2.get('age') + +const record3Get1: string = record3.get('name') +const record3Get2: number = record3.get('age') + +const record2Get3: string = record2.get('firstName') +const record2Get4: number = record2.get(1) -const record1Get2: object = record1.get(42) -const record2Get2: string[] = record2.get('key') +// @ts-expect-error +const record2Get5: any = record2.get('does-not-exist') diff --git a/types/record.d.ts b/types/record.d.ts index 63e437a48..c23b1c034 100644 --- a/types/record.d.ts +++ b/types/record.d.ts @@ -17,23 +17,38 @@ * limitations under the License. */ -declare type Visitor = (value: any, key: string, record: Record) => void +declare type Dict = { + [K in Key]: Value +} + +declare type Visitor< + Entries extends Dict = Dict, + Key extends keyof Entries = keyof Entries +> = MapVisitor -declare type MapVisitor = (value: any, key: string, record: Record) => T +declare type MapVisitor< + ReturnType, + Entries extends Dict = Dict, + Key extends keyof Entries = keyof Entries +> = (value: Entries[Key], key: Key, record: Record) => ReturnType -declare class Record { - keys: string[] +declare class Record< + Entries extends Dict = Dict, + Key extends keyof Entries = keyof Entries, + FieldLookup extends Dict = Dict +> { + keys: Key[] length: number constructor( - keys: string[], + keys: Key[], fields: any[], - fieldLookup?: { [index: string]: string } + fieldLookup?: FieldLookup ) - forEach(visitor: Visitor): void + forEach(visitor: Visitor): void - map(visitor: MapVisitor): T[] + map(visitor: MapVisitor): Value[] entries(): IterableIterator<[string, Object]> @@ -41,11 +56,13 @@ declare class Record { [Symbol.iterator](): IterableIterator - toObject(): object + toObject(): Entries + + get(key: K): Entries[K] - get(key: string | number): any + get(key: keyof FieldLookup | number): any - has(key: string | number): boolean + has(key: any): key is Key } export default Record