Skip to content

Instantly share code, notes, and snippets.

@dmorosinotto
Last active July 7, 2023 05:09
Show Gist options
  • Star 46 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save dmorosinotto/76a9272b5c45af1f78a61e7894df5777 to your computer and use it in GitHub Desktop.
Save dmorosinotto/76a9272b5c45af1f78a61e7894df5777 to your computer and use it in GitHub Desktop.
Typed @angular/forms FIXED set/patchValue - strict all the way down ^_^
import { FormGroup, FormControl, FormArray, Validators } from "@angular/forms";
function testFormGroupTyped() {
var frm = new FormGroup({
a: new FormArray([new FormControl(0)]),
b: new FormControl(true),
c: new FormGroup({
s: new FormControl("abc"),
n: new FormControl(123)
})
//d: new FormControl(null)
}) as FormGroupTyped<{ a: number[]; b: boolean; c: { s: string; n: number } /*; d?: {}*/ }>;
frm.status;
var x = frm.controls["a"].value; //OK infer number[]
var y = frm.controls.b.value; //OK infer boolean
frm.controls["c"].valueChanges.subscribe(z => console.log(z)); //OK infer {s: string, n:number}
var c = frm.get("c").value; //OK infer {s: string, n:number}
var s = frm.get(["c", "s"]).value; //OK infer unknown (Q: any is BETTER?!)
var u = frm.get("u").value; //OK infer unknow (Q: any is BETTER?!)
var r = frm.getRawValue(); //OK infer T + allow extra props
var a = r.a; //OK infer number[]
var d = r.d; //OK infer any
var ok = frm.contains("a"); //OK infer boolean
var ko = frm.contains("d"); //OK infer boolean -> true=enabled, false=disabled
frm.setValue({ a: [1, 23], b: true, c: { s: "s", n: 1 } }); // OK
frm.setValue({ b: false }, { onlySelf: true, emitEvent: false }); //ERROR -> setValue constraint to full T
frm.patchValue({ b: true }, { onlySelf: true, emitEvent: false }); //OK -> patchValue accept Partial<T>
frm.patchValue({ bb: true }, { onlySelf: true, emitEvent: false }); //ERROR -> not valid type Partial<T>
frm.removeControl("a"); //OK with auto-complete "a"
frm.removeControl("d"); //OK
frm.registerControl("a", new FormControl(null));
frm.registerControl("d", new FormControl(null)); //OK method with correct signature :-)
frm.get("a").setValue([1, 2]); //OK - valid constranint check number[]
frm.get("b").setValue("true"); //ERROR - invalid value boolean expected
frm.get("d").setValue({ z: "xy" }); //OK - any evrything is a valid assignment
var z: string = frm.get("d").value.z; //ERROR - but reading unknown force you to check before access
frm.get("b").valueChanges.subscribe(b => console.log(b)); //OK - infer boolean correctly without cast
frm.get("d").valueChanges.subscribe((u: { z: string }) => console.log(u.z)); //OK - explicit type cast to read from unknown
frm.get("d").value; //OK - anything is valid
frm.get("d").disable(); //OK method with correct signature :-)
frm.removeControl("b"); //OK with auto-complete "b" :-)
frm.removeControl("d"); //OK
frm.setControl("b", testFormControlTyped()); //OK method with strict signature :-)
frm.setControl("d", new FormControl(null)); //OK
frm.addControl("b", testFormControlTyped()); //OK
frm.addControl("a", testFormControlTyped()); //OK even if ovverride types :-()
frm.addControl("e", new FormControl(null)); //OK
frm.get(["e"]).reset(null, { onlySelf: true }); //OK method with correct signature :-)
}
function testFormArrayTyped() {
var arr = new FormArray([
new FormGroup({
s: new FormControl("abc"),
n: new FormControl(123)
})
]) as FormArrayTyped<{ s: string; n: number }>;
var x = arr.controls[0].value; //OK infer {s: string, n:number}
var y = arr.at(1).value; //OK infer {s: string; n:number}
arr.controls[0].valueChanges.subscribe(z => console.log(z)); //OK infer {s: string, n:number}
arr.push(itemFormGroup()); //OK method with strict signature :-)
arr.push(new FormControl(null)); //OK allowed by Angular -> infer AbstractControlTyped<any>
arr.insert(0, itemFormGroup()); //OK method with strict signature :-)
arr.setControl(2, new FormGroup({})); //OK allowed by Angular -> infer AbstractControlTyped<any>
arr.setControl(1, itemFormGroup()); //OK method with strict signature :-)
arr.removeAt(0); //OK
var l = arr.length; //OK
arr.setValue([{ s: "a", n: 1 }, { s: "b", n: 2 }]); //OK
arr.setValue([{ s: "a", n: 1 }, { n: 2 }]); //ERROR -> setValue constraint to full T[]
arr.patchValue([{ s: "a" }, { n: 2 }, {}]); //OK -> patchValue accept Partial<T>
arr.patchValue([{ s: "a" }, { n: "NOP" }]); //ERROR -> not valid type Partial<T>
arr.setValue([1, 23]); //ERROR -> invalid item type
arr.setValue(1.23); //ERROR -> invalid type :-)
}
function testFormControlTyped() {
var ctrl = new FormControl(true) as FormControlTyped<number>;
var x = ctrl.value; //OK infer boolean
ctrl.valueChanges.subscribe(z => console.log(z)); //infer boolean
ctrl.setValue("abc"); //ERROR invalid type
ctrl.patchValue(false); //OK correct type
return ctrl;
}
function itemFormGroup() {
var ctrl = new FormGroup({
s: new FormControl("", Validators.required),
n: new FormControl(0, [Validators.min(0), Validators.max(9)])
});
return ctrl as FormGroupTyped<{ s: string; n: number }>;
}
//BASIC TYPES DEFINED IN @angular/forms + rxjs/Observable
type FormGroup = import("@angular/forms").FormGroup;
type FormArray = import("@angular/forms").FormArray;
type FormControl = import("@angular/forms").FormControl;
type AbstractControl = import("@angular/forms").AbstractControl;
type Observable<T> = import("rxjs").Observable<T>;
type STATUS = "VALID" | "INVALID" | "PENDING" | "DISABLED"; //<- I don't know why Angular Team doesn't define it https://github.com/angular/angular/blob/7.2.7/packages/forms/src/model.ts#L15-L45)
type STATUSs = STATUS | string; //<- string is added only becouse Angular base class use string insted of union type https://github.com/angular/angular/blob/7.2.7/packages/forms/src/model.ts#L196)
//OVVERRIDE TYPES WITH STRICT TYPED INTERFACES + SOME TYPE TRICKS TO COMPOSE INTERFACE (https://github.com/Microsoft/TypeScript/issues/16936)
interface AbstractControlTyped<T> extends AbstractControl {
// BASE PROPS AND METHODS COMMON TO ALL FormControl/FormGroup/FormArray
readonly value: T;
valueChanges: Observable<T>;
readonly status: STATUSs;
statusChanges: Observable<STATUS>;
get<V = unknown>(path: Array<string | number> | string): AbstractControlTyped<V> | null;
setValue<V>(value: V extends T ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
patchValue<V>(value: V extends Partial<T> ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
reset<V>(value?: V extends Partial<T> ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
}
interface FormControlTyped<T> extends FormControl {
// COPIED FROM AbstractControlTyped<T> BECOUSE TS NOT SUPPORT MULPILE extends FormControl, AbstractControlTyped<T>
readonly value: T;
valueChanges: Observable<T>;
readonly status: STATUSs;
statusChanges: Observable<STATUS>;
get<V = unknown>(path: Array<string | number> | string): AbstractControlTyped<V> | null;
setValue<V>(value: V extends T ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
patchValue<V>(value: V extends Partial<T> ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
reset<V>(value?: V extends Partial<T> ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
}
interface FormGroupTyped<T> extends FormGroup {
// PROPS AND METHODS SPECIFIC OF FormGroup
//controls: { [P in keyof T | string]: AbstractControlTyped<P extends keyof T ? T[P] : any> };
controls: { [P in keyof T]: AbstractControlTyped<T[P]> };
registerControl<P extends keyof T>(name: P, control: AbstractControlTyped<T[P]>): AbstractControlTyped<T[P]>;
registerControl<V = any>(name: string, control: AbstractControlTyped<V>): AbstractControlTyped<V>;
addControl<P extends keyof T>(name: P, control: AbstractControlTyped<T[P]>): void;
addControl<V = any>(name: string, control: AbstractControlTyped<V>): void;
removeControl(name: keyof T): void;
removeControl(name: string): void;
setControl<P extends keyof T>(name: P, control: AbstractControlTyped<T[P]>): void;
setControl<V = any>(name: string, control: AbstractControlTyped<V>): void;
contains(name: keyof T): boolean;
contains(name: string): boolean;
get<P extends keyof T>(path: P): AbstractControlTyped<T[P]>;
getRawValue(): T & { [disabledProp in string | number]: any };
// COPIED FROM AbstractControlTyped<T> BECOUSE TS NOT SUPPORT MULPILE extends FormGroup, AbstractControlTyped<T>
readonly value: T;
valueChanges: Observable<T>;
readonly status: STATUSs;
statusChanges: Observable<STATUS>;
get<V = unknown>(path: Array<string | number> | string): AbstractControlTyped<V> | null;
setValue<V>(value: V extends T ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
patchValue<V>(value: V extends Partial<T> ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
reset<V>(value?: V extends Partial<T> ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
}
interface FormArrayTyped<T> extends FormArray {
// PROPS AND METHODS SPECIFIC OF FormGroup
controls: AbstractControlTyped<T>[];
at(index: number): AbstractControlTyped<T>;
push<V = T>(ctrl: AbstractControlTyped<V>): void;
insert<V = T>(index: number, control: AbstractControlTyped<V>): void;
setControl<V = T>(index: number, control: AbstractControlTyped<V>): void;
getRawValue(): T[];
// COPIED FROM AbstractControlTyped<T[]> BECOUSE TS NOT SUPPORT MULPILE extends FormArray, AbastractControlTyped<T[]>
readonly value: T[];
valueChanges: Observable<T[]>;
readonly status: STATUSs;
statusChanges: Observable<STATUS>;
get<V = unknown>(path: Array<string | number> | string): AbstractControlTyped<V> | null;
setValue<V>(value: V extends T[] ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
patchValue<V>(value: V extends Partial<T>[] ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
reset<V>(value?: V extends Partial<T>[] ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment