fixed-length array with optional items in Typescript interface
Asked Answered
A

1

7

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?

Arabella answered 9/6, 2017 at 15:31 Comment(0)
M
4

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.

Mexico answered 9/6, 2017 at 20:30 Comment(4)
Clever! And yeah I knew extra elements were possible but that's acceptable. (Also +1 for my AsyncValidatorFn miss.)Arabella
You can also specify the initial value as an object with a value property and a boolean disabled property. To cover that case using attempt 3, change 0: T[K] to 0: T[K]|{ value: T[K]; disabled: boolean }.Embay
late to the party but one can do simply type X = [string, string?, number?] to have optional typed array membersMantic
Thanks @AndreasHerd, I guess this wasn't a feature in 2017Fritts

© 2022 - 2024 — McMap. All rights reserved.