При проверке обтекта на наличие в свойстве массива, выдает ошибку

Есть обьект, я пытаюсь его типизировать и потом отчистить, но при переборе его и попытке его отчистить type script в упор не хочет видеть отличие строки от массива.

interface CoosingRole {
    name: string;
    code: string;
}

interface Rating {
    name: string;
    value: number;
}

interface Form {
    username: string
    email: string
    password: string
    password2: string
    file: string
    coosing_role: CoosingRole[]
    rating: Rating[]
}
const form = ref<Form>({
    username: '',
    email: '',
    password: '',
    password2: '',
    file: '',
    coosing_role: [{ name: 'USER', code: 'USER' }],
    rating: [{ name: '1', value: 1 }]
})
function clearForm() {

    Object.keys(form.value).forEach(key => {
        const field = form.value[key as keyof Form];

        if (Array.isArray(field)) {
            // Если поле - массив, очищаем его
            form.value[key as keyof Form] = [] as any;
        } else {
            // Если поле - строка, присваиваем пустую строку
            form.value[key as keyof Form] = ''; // Тип "string" не может быть назначен для типа "{ name: string; code: string; }[]".
        }
    });
}

Ответы (1 шт):

Автор решения: Alexandroppolus

На самом деле он ругался бы и на строку внутри if, но там богомерзкое as any..

TS никак не реагирует на косвенную проверку типа, потому что это для него пока сложно. Хотя Array.isArray - тайпгард, все равно тут получается слишком длинная цепочка рассуждений. В итоге внутри if и else тип для key так и остается строкой, которую ты потом приводишь к keyof Form.

Далее, есть разница между чтением const field = form.value[key as keyof Form] и записью form.value[key as keyof Form] = newValue. В первом случае имеем тип string | CoosingRole[] | Rating[] - мы ведь не знаем, какой там ключ. Во втором - newValue должен быть подходящим значением независимо от ключа, то есть должен быть string & CoosingRole[] & Rating[], разумеется ни строка, ни массив этому не соответствуют. Потому такое присвоение невозможно.

Вывод: надо написать тайпгард для ключа. Чтобы проверять, что ключ относится к тому набору ключей, по которым лежит значение заданного типа.

type FilterKeysByValue<O, V> = keyof {[K in keyof O as O[K] extends V ? K : never]: O[K];};

function isKeyForValue<T, O>(key: PropertyKey, obj: O, guard: (v: unknown) => v is T): key is FilterKeysByValue<O, T> {
    return obj != null && guard(obj[key as keyof O]);
}

function clearForm() {
    Object.keys(form.value).forEach(key => {
        if (isKeyForValue(key, form.value, Array.isArray)) {
            // Если поле - массив, очищаем его
            form.value[key] = [];
        }
        if (isKeyForValue(key, form.value, (v) => typeof v === 'string')) {
            // Если поле - строка, присваиваем пустую строку
            form.value[key] = '';
        }
    });
}

Здесь FilterKeysByValue<O, V> - генерик, который для типа O возвращает набор тех ключей, по которым значение соответствует типу V

isKeyForValue - тот самый тайпгард для проверки ключа, ему дополнительно передаем объект и тайпгард проверки значения.

Ссылка на playground

Если у тебя версия TS меньше 5.5, то строку (v) => typeof v === 'string' замени на (v): v is string => typeof v === 'string'

→ Ссылка