Сделать типизированные формы более последовательными

Типизированные формы Angular — это очень здорово. Сильная типизация форм дает много преимуществ, но одна проблема сдерживает ее: возможность определить единый интерфейс, на основе которого можно создать типизированную форму и вывести из нее значение формы.

Рассмотрим следующий код:

interface UserFormControls {
  firstName: FormControl<string>;
  lastName: FormControl<string>;
  email: FormControl<string | null>;
  age: FormControl<number | null>;
}

interface User {
  firstName: string;
  lastName: string;
  email: string | null;
  age: number | null;
}

function processUser(user: User): void {
    // ...
}

const userForm = new FormGroup<UserFormControls>({
    firstName: new FormControl('foo', { nonNullable: true }),
    lastName: new FormControl('bar', { nonNullable: true }),
    email: new FormControl('foo@bar.com', { nonNullable: true }),
    age: new FormControl(null)
});

processUser(userForm.value); // This won't actually compile, keep reading
Войти в полноэкранный режим Выход из полноэкранного режима

В идеале, вы не хотите быть вынуждены поддерживать два отдельных интерфейса, определяющих одно и то же. Пользовательский интерфейс может быть выведен из UserFormControls, так что давайте сделаем это. Для этого мы используем два новых типа.

type FormValue<T extends AbstractControl> = 
    T extends AbstractControl<infer TValue, any>
    ? TValue
    : never;
type FormRawValue<T extends AbstractControl> = 
    T extends AbstractControl<any, infer TRawValue> 
    ? TRawValue 
    : never;
Вход в полноэкранный режим Выход из полноэкранного режима

Давайте посмотрим, что произойдет, когда мы применим их к нашим UserFormControls.

interface UserFormControls {
    firstName: FormControl<string>;
    lastName: FormControl<string>;
    email: FormControl<string | null>;
    age: FormControl<number | null>;
}
type UserForm = FormGroup<UserFormControls>;
type User = FormValue<UserForm>;
// type User = {
//  firstName?: string | undefined;
//  lastName?: string | undefined;
//  email?: string | null | undefined;
//  age?: number | null | undefined;
// }

type UserRaw = FormRawValue<UserForm>;
// type UserRaw = {
//  firstName: string;
//  lastName: string;
//  email: string | null;
//  age: number | null;
// }
Вход в полноэкранный режим Выход из полноэкранного режима

Обратите внимание, что тип User теперь имеет все свойства как необязательные. Это сделано потому, что элементы управления могут быть отключены, и они не будут отображаться в окончательном значении формы. Необработанное значение набирается точно так же, как мы указывали интерфейс User ранее. Именно поэтому processUser(userForm.value); в первом блоке кода не будет компилироваться.

Сделайте свой выбор

Здесь вы должны сделать выбор:

  • Вы можете либо использовать FormValue<...> и иметь дело с каждым свойством, которое потенциально неопределено, либо;
  • Использовать FormRawValue<...> с осторожностью. Если все элементы управления, которые могут быть отключены, помечены как необязательные, ваш ввод будет правильным.

Я бы рекомендовал последнее. В этом случае мы получим следующее решение:

type User = FormRawValue<UserForm>;
// type User = {
//  firstName: string;
//  lastName: string;
//  email: string | null;
//  age: number | null;
// }

// ...

function processUser(user: User): void {
    // ...
}

processUser(userForm.value as User);
// or:
processUser(userForm.getRawValue());
Войти в полноэкранный режим Выйти из полноэкранного режима

Удачи!

Оцените статью
devanswers.ru
Добавить комментарий