Skip to content

Commit 150d8c5

Browse files
author
Sébastien
authored
Better types for immutables (#16)
* Stricter types for immutables * Moar goodies * moar
1 parent 4c66da0 commit 150d8c5

File tree

4 files changed

+39
-19
lines changed

4 files changed

+39
-19
lines changed

src/base-immutable/base-immutable.spec.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ describe('BaseImmutable', () => {
131131
name: 'focus2',
132132
},
133133
},
134-
} as any);
134+
});
135135

136136
const newCar = car.deepChange('subCar.subCar', { name: 'anuford' });
137137
expect(newCar.deepGet('subCar.subCar.name')).toEqual('anuford');
@@ -233,7 +233,7 @@ describe('BaseImmutable', () => {
233233
name: 'focus2',
234234
},
235235
},
236-
} as any);
236+
});
237237
expect(car.deepGet('subCar.subCar.name')).toEqual('focus2');
238238
});
239239

@@ -242,12 +242,26 @@ describe('BaseImmutable', () => {
242242
fuel: 'electric',
243243
name: 'ford',
244244
owners: [],
245-
} as any);
245+
});
246246

247247
expect(car.toJS()).toEqual({
248248
fuel: 'electric',
249249
name: 'ford',
250250
owners: [],
251251
});
252252
});
253+
254+
it('works with types', () => {
255+
const car = Car.fromJS({
256+
fuel: 'electric',
257+
name: 'ford',
258+
});
259+
260+
// should pass TS transpilation
261+
car.change('owners', ['foo']);
262+
263+
// should NOT pass TS transpilation
264+
// car.change('owners', 'foo');
265+
// car.change('some unknown prop', ['hello']);
266+
});
253267
});

src/base-immutable/base-immutable.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ export const PropertyType = {
4343
ARRAY: 'array' as PropertyType,
4444
};
4545

46-
export interface Property {
47-
name: string;
46+
export interface Property<T extends { [key: string]: any } = any> {
47+
name: keyof T;
4848
defaultValue?: any;
4949
possibleValues?: any[];
5050
validate?: Validator | Validator[];
@@ -82,8 +82,8 @@ export abstract class BaseImmutable<ValueType, JSType>
8282
// This needs to be defined
8383
// abstract static PROPERTIES: Property[];
8484

85-
static jsToValue(
86-
properties: Property[],
85+
static jsToValue<T = any>(
86+
properties: Property<T>[],
8787
js: any,
8888
backCompats?: BackCompat[],
8989
context?: Record<string, any>,
@@ -222,16 +222,16 @@ export abstract class BaseImmutable<ValueType, JSType>
222222
}
223223
}
224224

225-
public ownProperties(): Property[] {
225+
public ownProperties(): Property<ValueType>[] {
226226
return (this.constructor as any).PROPERTIES;
227227
}
228228

229-
public findOwnProperty(propName: string): Property | undefined {
229+
public findOwnProperty(propName: keyof ValueType): Property | undefined {
230230
const properties = this.ownProperties();
231231
return NamedArray.findByName(properties, propName);
232232
}
233233

234-
public hasProperty(propName: string): boolean {
234+
public hasProperty(propName: keyof ValueType): boolean {
235235
return this.findOwnProperty(propName) !== null;
236236
}
237237

@@ -319,27 +319,30 @@ export abstract class BaseImmutable<ValueType, JSType>
319319
return true;
320320
}
321321

322-
public get(propName: string): any {
322+
public get<T extends keyof ValueType>(propName: T): ValueType[T] {
323323
const getter = (this as any)['get' + firstUp(propName)];
324324
if (!getter) throw new Error(`can not find prop ${propName}`);
325325
return getter.call(this);
326326
}
327327

328-
public change(propName: string, newValue: any): this {
328+
public change<T extends keyof ValueType>(propName: T, newValue: ValueType[T]): this {
329329
const changer = (this as any)['change' + firstUp(propName)];
330330
if (!changer) throw new Error(`can not find prop ${propName}`);
331331
return changer.call(this, newValue);
332332
}
333333

334-
public changeMany(properties: Record<string, any>): this {
334+
public changeMany(properties: Partial<ValueType>): this {
335335
if (!properties) throw new TypeError('Invalid properties object');
336336

337337
let o = this;
338338

339339
for (const propName in properties) {
340340
if (!this.hasProperty(propName)) throw new Error('Unknown property: ' + propName);
341341

342-
o = o.change(propName, properties[propName]);
342+
// Added ! because TypeScript thinks a Partial can have undefined properties
343+
// (which they can and it's cool)
344+
// https://github.com/Microsoft/TypeScript/issues/13195
345+
o = o.change(propName, properties[propName]!);
343346
}
344347

345348
return o;

src/base-immutable/car.mock.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ export interface CarValue {
4747
range?: number;
4848
relatedCars?: Car[];
4949
createdOn?: Date;
50+
owners?: string[];
51+
driver?: Driver;
5052
}
5153

5254
export interface CarJS {
@@ -56,14 +58,16 @@ export interface CarJS {
5658
range?: number;
5759
relatedCars?: CarJS[];
5860
createdOn?: Date | string;
61+
owners?: string[];
62+
driver?: DriverJS;
5963
}
6064

6165
function ensureNonNegative(n: any): void {
6266
if (n < 0) throw new Error('must non negative positive');
6367
}
6468

6569
export class Car extends BaseImmutable<CarValue, CarJS> {
66-
static PROPERTIES: Property[] = [
70+
static PROPERTIES: Property<CarValue>[] = [
6771
{
6872
name: 'name',
6973
validate: (n: string) => {

tsconfig.json

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@
1414
"target": "es5",
1515
"rootDir": "src",
1616
"outDir": "build",
17-
"moduleResolution": "node"
17+
"moduleResolution": "node",
18+
"keyofStringsOnly": true
1819
},
19-
"include": [
20-
"src/**/*.ts"
21-
]
20+
"include": ["src/**/*.ts"]
2221
}

0 commit comments

Comments
 (0)