Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

fixed-length array with optional items in Typescript interface

Tags:

Angular introduced Model-driven forms with its FormBuilder class, whose primary method group has a signature like this:

group(controlsConfig: {
        [key: string]: any;
    }): FormGroup;

The any is actually an array with the format:

[
    initial value of model's property, 
    sync validator(s), 
    async validator(s)
]

Where only the first element is required.

I decide I'd like something a little more strongly typed than that, particularly on anything which is associated with a strongly typed Model, so I re-define the function in terms of T:

declare interface FormBuilder2 extends FormBuilder {
    group<T>(controlsConfig: {
        [K in keyof T]?: [T[K], ValidatorFn | ValidatorFn[] | null, ValidatorFn | ValidatorFn[] | null];
    }): FormGroup;
}

This also means that all my formControlNames in the HTML (and of course here in the group() call) must match the model's properties, which I prefer.

It seems to work but for one snafu:

    this.optionsForm = this.formBuilder2.group<CustomerModel>({
        status:    [this.model.status, [Validators.required], null],
        lastOrder: [this.model.lastOrder, null, null],
        comments:  [this.model.comments, null, null],
    });

I must provide null on the unused array slots.

Is there a way to get Typescript to omit the need for the extraneous nulls?

like image 704
Ron Newcomb Avatar asked Jun 09 '17 15:06

Ron Newcomb


1 Answers

There's no really type-safe way to do this with tuple types, because of the way tuples can accept extra elements. That is, for example, the tuple type [A, B, C] will actually accept additional elements of type A | B | C (see docs).

However, there is a solution! (See attempt 3 below)

(By the way, you've overlooked that Angular has a difference interface for async validators: AsyncValidatorFn.)

Attempt 1:

[K in keyof T]?: [T[K] | ValidatorFn | ValidatorFn[] | null];

Hardly better than any typing (possibly worse, because it looks misleadingly meaningful).

Attempt 2:

[K in keyof T]?:
  [T[K]] |
  [T[K], ValidatorFn | ValidatorFn[]] |
  [T[K], ValidatorFn | ValidatorFn[] | null, AsyncValidatorFn | AsyncValidatorFn[]];

Seems better at first glance. But the problem is the Typescript compiler will only throw an error as a last resort. So it will accept this:

someStringField: ['hi', 'hello']

Because this conforms to [T[K]] (as tuples in Typescript are allowed to have extra elements).

Attempt 3:

There is a better solution, much to my amazement. I found out about this halfway through writing this answer, while reading this issue on the Typescript GitHub repo.

[K in keyof T]?: {
  0: T[K];
  1?: ValidatorFn | ValidatorFn[];
  2?: AsyncValidatorFn | AsyncValidatorFn[];
};

This is an improvement on the previous attempt in that the first three elements are always type-checked properly. ['hi', 'hello'] gives a compile error, correctly. Additional elements are allowed and can be anything, as per usual structural typing, but that's ok.

Hope this solves your problem.

like image 180
dbandstra Avatar answered Nov 15 '22 07:11

dbandstra