Следует ли использовать дженерики Typescript для принудительного использования интерфейса при вызове метода?

Пытаюсь разобраться с дженериками и задаюсь вопросом, правильно ли я применяю их здесь.

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

interface NameValuePair {
    name: string;
    value: string;
}

function flatten(data: NameValuePair[]) {
    return data.reduce((obj, pair: NameValuePair) => {
        obj[pair.name] = pair.value;
        return obj;
    }, {});
}

const formData: NameValuePair[] = [
    { name: "firstName", value: "John" },
    { name: "lastName", value: "Doe" }
];

let flattened = flatten(formData);  
// { firstName: "John", lastName: "Doe" }  

Всякий раз, когда вызывается функция, я бы хотел, чтобы Typescript принудительно использовал настраиваемый интерфейс, описывающий окончательную структуру данных. Например, используя formData сверху, возможный интерфейс может выглядеть так:

interface ProfileForm {
    firstName: string;
    lastName: string;
}

Ниже приведена моя попытка использовать дженерики:

function flatten<T>(formData: NameValuePair[]): T {
    return <T>formData.reduce((obj: T, pair: NameValuePair) => {
        obj[pair.name] = pair.value;
        return obj;
    }, {});
}

let flattened = flatten<ProfileForm>(formData);
let firstName = flattened.firstName;
let age = flattened.age; // typescript error

Он работает, как и ожидалось, но при тестировании он дает те же результаты, что и:

function flatten(formData: NameValuePair[]) {
    return formData.reduce((obj, pair: NameValuePair) => {
        obj[pair.name] = pair.value;
        return obj;
    }, {});
}

let flattened = <ProfileForm>flatten(formData);
let firstName = flattened.firstName;
let age = flattened.age; // typescript error

В данном конкретном случае дают ли дженерики какую-либо пользу?


person jbarreiros    schedule 13.10.2018    source источник
comment
Я пишу ответ, но можете ли вы сказать, каково ваше истинное намерение? Что вы пытаетесь решить с помощью TypeScript? Зачем вам такие интерфейсы?   -  person Nurbol Alpysbayev    schedule 13.10.2018
comment
Основная идея состоит в том, чтобы заставить любого, кто использует функцию flatten(), предоставить интерфейс для отображения возвращаемого значения. В частности, flatten() предназначен для преобразования формы в объект JSON. Каждая форма отличается, с различными полями. Я полагал, что, поскольку Typescript является типизированным языком, именно для этого и был написан язык. Не слишком ли я далеко зашел в идее принудительного ввода типов?   -  person jbarreiros    schedule 13.10.2018
comment
Ok. Вы пробовали решение в моем ответе?   -  person Nurbol Alpysbayev    schedule 14.10.2018


Ответы (2)


Это работает

const formDataRaw: NameValuePair[] = [{ name: "fullName", value: "John Doe" }]

// please note that reduce() was unnecessary, or you used it wrong. 
// Anyway, your question should be focused, TypeScript and reduce() is 
// two unrelated things, especially in your code I didn't see how they 
// were related

const formData: NameValuePair = formDataRaw[0]

interface NameValuePair {
    name: keyof ProfileForm | keyof OtherForm;
    value: string;
}

interface ProfileForm {
    fullName: string;
}

interface OtherForm {
    otherProp: string;
}

type ResultingForm<NVP extends NameValuePair> =
    NVP['name'] extends keyof ProfileForm ? ProfileForm :
    NVP['name'] extends keyof OtherForm ? OtherForm :
    never

interface Flatten {
    <NVP extends NameValuePair>(formData: NVP): ResultingForm<NVP>
}

const flatten: Flatten = <NVP extends NameValuePair>(obj: NVP) => {
    return { [obj['name']]: obj['value'] } as unknown as ResultingForm<NVP>
}

flatten(formData) // <--ok
flatten({ name: 'fullName', value: 'zxc' }) // <--ok. Code Completion works
flatten({ name: 'otherProp', value: 'zxc' }) // <--ok. Code Completion works
flatten({ name: 'foo', value: 'zxc' }) // <-- error
person Nurbol Alpysbayev    schedule 13.10.2018
comment
Я не уверен, что это дает результат, который я ищу. В частности, flatten() должен принимать массив объектов, а не один объект. Я переработал свой первоначальный вопрос, чтобы, надеюсь, сделать его более ясным. - person jbarreiros; 15.10.2018

Ниже приведена моя попытка использовать дженерики:

function flatten<T>(formData: NameValuePair[]): T {
  return <T>formData.reduce((obj: T, pair: NameValuePair) => {
    obj[pair.name] = pair.value;
    return obj;
  }, {});
}

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

function flatten(formData: NameValuePair[]): ProfileForm {
  return formData.reduce((obj: T, pair: NameValuePair) => {
    obj[pair.name] = pair.value;
    return obj;
  }, {}) as ProfileForm;
}

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

Так что в этом случае не используйте дженерики. Тогда вы можете спросить Что именно предоставляет Generics и почему я должен его использовать .

person Erik Philips    schedule 15.10.2018
comment
Если бы ProfileForm был единственным возможным интерфейсом, использование его для возвращаемого типа имеет смысл, но интерфейсов может быть сколько угодно. Например, interface LoginForm { username: string; password: string; } или interface SettingsForms { timezone: string; }. В таком случае имеет ли смысл возвращать flatten()s typehint как пустой интерфейс, а затем каждый конкретный интерфейс расширяет его? Например, interface Form {} и interface ProfileForm extends Form { ... }? - person jbarreiros; 15.10.2018
comment
Основываясь на вашем комментарии, вы пытаетесь свернуть код во что-то, что делает несколько вещей, что как бы нарушает принцип единой ответственности. У вас должен быть метод, который извлекает свойства, необходимые для каждого типа. - person Erik Philips; 15.10.2018