Не удается получить доступ к свойству в объединении типов объектов для свойств, не определенных для всех членов объединения

Я сталкиваюсь со сценарием, когда ответ на HTTP-вызов зависит от региона. Я указал возвращаемый тип объекта. Итак, если я объявлю предположительные 4 типа и использую их объединение в качестве типа-оболочки.

Проблема возникает, так как есть поля, которые не являются общими для всего. Это решение состоит в том, чтобы сделать эти поля необязательными. Для меня сделать поле необязательным означает, что в нем нет необходимости, что неверно в данном случае. Как это сделать, чтобы ошибка Tslint исчезла.

Пожалуйста, скажите мне, если вы не понимаете мой вопрос

РЕДАКТИРОВАТЬ:-

function mapAddress(address: AddressRegionXX | AddressRegionYY,region:string): AddressWithIdXX | AddressWithIdXX   {
  let addressId = address.id ? address.id : "XX";

  let addressType = addressId == "XX" ? "subbed" : "unsubbed";
 if(region == "XX"){
  return {
    firstName: address.first_name || null,
    lastName: address.last_name || null,
    street1: address.addr_1 || null,
    street2: address.addr_2 || null,
    city: address.city || null,
    state: address.state || null,
    postalCode: address.zip_code || null,
    phone: address.phone_number || null,
    addressId: addressId,
    addressType: addressType
  };
   if(region == "XX"){
  return {
    fName: address.f_name || null,
    lName: address.l_name || null,
    address: address.addr_1 || null,
    roomNo: address.addr_2 || null,
    district: address.district|| null,
    state: address.state || null,
    pinCode: address.zip_code || null,
    phoneNumber: address.phone_number || null,
    addressId: addressId,
    addressType: addressType
  };
 }
}

Это контекст, в котором я должен использовать тип Union. Здесь ответ в зависимости от типа адреса каждого региона будет меняться, есть длинный список, который нецелесообразно включать здесь. Как я показал здесь, имена полей различаются для каждого региона и для некоторых дополнительных полей. Итак, каков элегантный способ справиться с этой ситуацией, правильно использовать условные типы. Есть ли альтернатива типу union. Как и в ened, будет по крайней мере 5-6 типов адресов и шансы на то, что в будущем их станет больше.

In layman terms 
is there any miraculous way in which :D 
We write something Like
type correctTypeAddress<T> =
    T extends Address? AddressXX :
    T extends Address? AddressYY :

mapAddress(адрес: AddressRegion,region:string):correctTypeAddress

Ниже приведен пример того, что я имею дело со всеми типами, не имеющими одинаковых свойств. Итак, как работать с неоднородными отображениями типов. Есть ли альтернатива использованию типа объединения, когда

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

type typeA = {
  prop1:string;
  prop2:string;
}

type typeB = {
  prop1: string;
  prop3: string;
}
type typeC = {
  prop4: string;
  prop5: string;
}
type mappedType = typeA | typeB | typeC;

const a = (b): mappedType => {

  return {
    prop1:"1",
    prop5:"3"
  }
}

EDIT:- Применение условных типов, но использование универсальных приводит к другой ошибке lint как Property 'prop1' does not exist on type 'T'

type typeIA = {
  prop1: string;
  prop2: string;
}

type typeIB = {
  prop1: string;
  prop3: string;
}
type typeIC = {
  prop4: string;
  prop5: string;
}

type typeOA = {
  prop1: string;
  prop2: string;
}

type typeOB = {
  prop1: string;
  prop3: string;
}
type typeOC = {
  prop4: string;
  prop5: string;
}
// type mappedType = typeA | typeB | typeC;

const a = <T extends typeIA | typeIB | typeIC>(_b: T): T extends typeIA ? typeOA : never | T extends typeIB ? typeOB : never | T extends typeIC ? typeOC : never=> {
  if (_b.prop1 == "1"){
   return {
     prop1: "1",
     prop3: "3"
   } as T extends typeIA ? typeOA : never | T extends typeIB ? typeOB : never | T extends typeIC ? typeOC : never
 }else{
    return {
      prop1: "1",
      prop2: "2"
    } as T extends typeIA ? typeOA : never | T extends typeIB ? typeOB : never | T extends typeIC ? typeOC : never
 }

}
const c = a({prop1:"1",prop2:"2"});

const d = a({ prop1: "1", prop3: "2" });

const e = a({ prop4: "1", prop5: "2" });

person codefreaK    schedule 28.04.2019    source источник
comment
просто используйте защиту типа ?   -  person Austaras    schedule 29.04.2019
comment
Пожалуйста, отредактируйте этот вопрос, чтобы предоставить минимально воспроизводимый пример вашей проблемы, чтобы кто-то мог сделать предложение, которое непосредственно решает ее. . В противном случае любой ответ будет просто предположением о реальной проблеме, с которой вы столкнулись.   -  person jcalz    schedule 29.04.2019
comment
@jcalz не могли бы вы взглянуть сейчас   -  person codefreaK    schedule 06.05.2019
comment
Я бы с удовольствием посмотрел на это, но барьер для входа все еще слишком высок. Чтобы этот код даже скомпилировался без ошибок, чтобы начать работу над ответом, кому-то нужно будет изобрести определения интерфейса/типа для таких типов, как AddressRegion и т. д. Не могли бы вы это сделать? В идеале вы должны предоставить нам некоторый код, который кто-то может добавить в IDE как есть (например, Playground) и начать вносить изменения и предложения. Единственными ошибками должны быть те, которые имеют отношение к проблеме. Это полные и поддающиеся проверке части минимально воспроизводимого примера. Удачи!   -  person jcalz    schedule 06.05.2019
comment
@jcalz больше, чем исправление этой конкретной ошибки, я искал способ использования условных типов для преодоления использования типа объединения, если это имеет смысл   -  person codefreaK    schedule 06.05.2019
comment
В идеале вопросы и ответы должны выглядеть так: В: Вот код, который утомительный и не масштабируется. Есть ли лучший способ сделать это? A: Вот измененный код, который имеет тот же эффект, но менее утомительный и лучше масштабируется. Я прошу вас сделать часть Q. Если ваш код примера полон ошибок, то либо я должен исправить их сам, либо я должен принять ошибки в коде ответа, что означает, что я не могу проверить ответ, а значит, возможно, он не будет работать. Лично мне неудобно предлагать ответ, который я не могу проверить. Может быть, придет кто-то еще, кто думает по-другому.   -  person jcalz    schedule 06.05.2019
comment
@jcalz Я понимаю, откуда ты. Я бы постарался, чтобы это соответствовало вашим стандартам :D   -  person codefreaK    schedule 06.05.2019
comment
линтер выдаст ошибку в VScode, когда вы вернете что-то, что не имеет всех свойств во всех типах. Надеюсь, вы понимаете контекст   -  person codefreaK    schedule 06.05.2019
comment
Извините, я до сих пор нигде не вижу минимально воспроизводимого примера. У вас есть текст, отформатированный как код, код, отформатированный как текст, и единственный код, который не вызывает ошибок, — это объединение в конце, и вы не используете его ни для чего. Я предлагаю вам внимательно ознакомиться с рекомендациями по как задать хороший вопрос. Желаю вам удачи в получении ответа, и я сожалею, что не могу быть более полезным.   -  person jcalz    schedule 07.05.2019


Ответы (2)


Полное объяснение того, почему это «лучший» способ (шучу, нет лучшего способа), вероятно, выходит за рамки ответа на переполнение стека, но, по сути, несколько быстрых моментов:

  1. чтобы получить keyof union типов, вам нужно превратить его в пересечение keyof (A | B) с keyof (A & B)

  2. Использование «Может быть», в котором у вас может быть два возможных результата, удваивается как решение для undefined/null, однако в этом случае он решает, что при выборе из союза вы можете иметь «Что-то», которое будет типом A или типом B или ничего, что имеет место тип C

  3. Условные возвращаемые типы, как правило, хуже, чем перегрузка, просто потому, что во второй раз, когда у вас есть условный возвращаемый тип, вы в значительной степени приговариваете себя к тому, чтобы каким-то образом привести возвращаемое значение.
  4. Узнайте больше о Scala 'Option' или Haskells 'Maybe', на котором это решение основано где-то еще, выходит ли это за рамки этого ответа, но это решение в основном украло его лучшие идеи.

Получен фрагмент длинного кода. Надеюсь, это поможет, это будет казаться чрезмерным, но это не так.

export type UnionToIntersection<U> = [U] extends [never]
    ? never
    : (U extends any ? (k: U) => void : never) extends ((k: infer I) => void)
        ? I
        : never;
export type UnionMembersWith<T, K extends keyof UnionToIntersection<T>> = [T] extends [never]
    ? never
    : Exclude<T, Exclude<T, Partial<Record<K, any>>>>;

export type Maybe<A> = _None<A> | _Some<A>;

type PickReturn<A, K extends keyof UnionToIntersection<A>> = [K] extends [never]
    ? typeof None
    : [K] extends [keyof UnionMembersWith<A, K>]
        ? Maybe<NonNullable<UnionMembersWith<A, K>[K]>>
        : [K] extends [keyof A]
            ? Maybe<NonNullable<A[K]>>
            : typeof None


class _Some<A> {
    readonly _tag: "some" = "some";
    readonly value: A;
    constructor(value: A) {
      this.value = value;
    }
    map<U>(f: (a: A) => U): Maybe<U> {
      return new _Some(f(this.value));
    }

    flatMap<B>(f: (a: A) => Maybe<B>): Maybe<B> {
      return f(this.value);
    }

    pick<K extends keyof UnionToIntersection<A>>(key: K): PickReturn<A, K> {
      return Maybe((this.value as any)[key]) as any;
    }

    get get(): A | undefined {
        return this.value;
    }
}

class _None<A = never> {
    static value: Maybe<never> = new _None();
    readonly _tag = "none";

    map<U>(f: (a: A) => U): Maybe<U> {
        return this as any;
    }

    flatMap<B>(f: (a: A) => Maybe<B>): Maybe<B> {
        return this as any;
    }

    pick<K extends keyof UnionToIntersection<A>>(key: K): PickReturn<A, K> {
        return this as any;
    }

    get get(): A | undefined {
        return undefined;
    }

    getOrElse(none: never[]): [A] extends [Array<any>] ? A : A | never[];
    getOrElse<B>(none: B): A | B;
    getOrElse<B>(none: B): A | B {
        return none as any;
    }
}

export const None: Maybe<never> = _None.value;
export const Some = <A>(a: A): _Some<A> => new _Some(a);

export function Maybe<A>(value: A | null | undefined): Maybe<A> {
    if (value !== null && value !== undefined) return Some(value);
    return None;
}

//* END IMPLEMNTATION */


type typeIA = {
  prop1: string;
  prop2: string;
}

type typeIB = {
  prop1: string;
  prop3: string;
}
type typeIC = {
  prop4: string;
  prop5: string;
}

type typeOA = {
  prop1: string;
  prop2: string;
}

type typeOB = {
  prop1: string;
  prop3: string;
}
type typeOC = {
  prop4: string;
  prop5: string;
}
// type mappedType = typeA | typeB | typeC;

function a(_b: typeIC): typeOC
function a(_b: typeIB): typeOB
function a(_b: typeIA): typeOA
function a(_b: typeIA | typeIB | typeIC): typeOA | typeOB | typeOC {
    /* 100% typesafe */
  if (Maybe(_b).pick("prop1").get === "1"){
   return {
     prop1: "1",
     prop3: "3"
   }
 }else{
    return {
      prop1: "1",
      prop2: "2"
    }
 }

}

const c = a({prop1:"1",prop2:"2"}); // type oA
const d = a({ prop1: "1", prop3: "2" }); // type oB
const e = a({ prop4: "1", prop5: "2" }); // type oC

РЕДАКТИРОВАТЬ: «Знать больше»: возможно, это не решение этой разовой проблемы для вас, а решение определенного типа «эффекта», который может возникнуть в программировании. Maybe является монадическим и монадным эффектом ловушки, эффект Maybe traps — это недетерминизм, и идея здесь в том, что Maybe абстрагирует его, так что вам не нужно об этом думать.

Этот момент можно легко упустить, потому что любой другой переполненный пользователь будет утверждать, что это можно легко решить, вложив «Если/иначе», но идея «Может быть» заключается в том, что это абстракция, в которой вам больше не нужно проверять, если вещи ЯВЛЯЮТСЯ или НЕТ там и, следовательно, больше не нуждаются в If/Else

так много слов, на что это похоже? Таким образом, этот код безопасен как во время выполнения, так и на уровне типов.

interface IPerson {
    name: string;
    children: IPerson[] | undefined;
}

const person = {
    name: "Sally",
    children: [
        {
            name: "Billy",
            children: [
                {
                    name: "Suzie",
                    children: undefined
                }
            ]
        }
    ]
};

const test = Maybe(person).pick("children").pick(0).pick("children").pick(0).get;  // Typesafe / Runtime safe possible path
const test = Maybe(person).pick("children").pick(0).pick("children").pick(10000).get ; // Typesafe / Runtime safe impossible paths

/* We have 'Children' which is non-deterministic it could be undefined OR it could be defined */
/* Let's wrap person in Maybe so we don't care whther its there or not there anymore */
const test2 = Maybe(person).pick("children").map((childrenArr) => {
    return childrenArr.map((child) => child.name.toUpperCase())
}).getOrElse([]);  // string[] notice how when using things in the context of our Maybe we cease to care about undefined.


const test3 = Maybe(person).pick("children").pick(10000).map((person) => {
    return {...person, name: person.name.toUpperCase()} // safe even though there's no person at Array index 10000
}).getOrElse({name: "John Doe", children: []})   // IPerson even though there is no person at Array index 10000
person Shanon Jackson    schedule 10.05.2019
comment
Шэнон, я очень заинтригован твоим ответом, но, честно говоря, я не в теме. Я пытаюсь обернуть голову вокруг вашего ответа. Было бы здорово, если бы вы могли добавить комментарии, чтобы объяснить то же самое. Если это не слишком много, чтобы спросить - person codefreaK; 10.05.2019
comment
Это очень сложно объяснить, но я сделал все возможное, основной ответ заключается в том, что Maybe позволяет вам никогда не использовать if/else или даже вложенные if/else при выполнении недетерминированных действий, таких как доступ к свойствам членов союза, когда не все они имеют это свойство и, например, используют значения, которые могут быть нулевыми | неопределенный - person Shanon Jackson; 11.05.2019
comment
Да уже получил эту часть. Я думал построить возможно сам. Так что я мог понять это сам. Я думал в том же духе об использовании пересечения, которое означало бы, что все свойства будут доступны. Но на самом деле это было бы неправильно, поскольку должно быть либо TypeIA, либо TypeIB, либо TypeIC. Но, может быть, это было бы отличным решением загадки - person codefreaK; 11.05.2019

Я вижу следующие варианты (вы можете комбинировать их):

  1. Перегрузка функций

    interface AddressRegionXX {
        id: string;
        first_name?: string;
        last_name?: string;
    }  
    
    interface AddressRegionYY {
        f_name?: string;
        l_name?: string;
    }
    
    interface AddressWithIdXX {
        firstName: string | null;
        lastName: string | null;
        addressId: string;
        addressType: string;
    }
    
    interface AddressWithIdYY {
        fName: string | null;
        lName: string | null;
        addressId: string;
        addressType: string;
    }
    
    function mapAddress(address: AddressRegionXX, region: string): AddressWithIdXX;
    function mapAddress(address: AddressRegionYY, region: string): AddressWithIdYY;
    function mapAddress(address: AddressRegionXX | AddressRegionYY, region: string): AddressWithIdXX | AddressWithIdYY {
        let addressId = (<AddressRegionXX>address).id ? (<AddressRegionXX>address).id : "XX";
    
        let addressType = addressId == "XX" ? "subbed" : "unsubbed";
        if (region == "XX") {
            let AddressRegion = <AddressRegionXX>address;
            return {
                firstName: AddressRegion.first_name || null,
                lastName: AddressRegion.last_name || null,
                addressId: addressId,
                addressType: addressType
            };
        }
        if (region == "YY") {
            let AddressRegion = <AddressRegionYY>address;
            return {
                fName: AddressRegion.f_name || null,
                lName: AddressRegion.l_name || null,
                addressId: addressId,
                addressType: addressType
            };
        }
    }
    
    // Usage
    let aXX: AddressRegionXX = { id: "idXX" };
    let resXX: AddressWithIdXX = mapAddress(aXX, "XX");
    
    let aYY: AddressRegionYY = { };
    let resYY: AddressWithIdYY = mapAddress(aYY, "YY");
    

    Хороший момент здесь - иметь результат точного типа в зависимости от типа агрумента. Для address типа AddressRegionXX вы получите результат типа AddressWithIdXX.

    Проблема с этим подходом заключается в том, что вы должны быть очень осторожны при реализации функции mapAddress. Код внутри него ничего не знает о типе address, поэтому вам следует учитывать некоторые дополнительные условия. (Код машинописного текста будет перенесен в JavaScript, и все интерфейсы будут удалены).

  2. Используйте размеченные союзы.

    enum TypeDiscriminant {
        addressXX,
        addressYY
    }
    
    interface AddressRegionXX {
        TypeDiscriminant: TypeDiscriminant.addressXX;
        id: string;
        first_name?: string;
        last_name?: string;
    }
    
    interface AddressRegionYY {
        TypeDiscriminant: TypeDiscriminant.addressYY;
        f_name?: string;
        l_name?: string;
    }
    
    interface AddressWithIdXX {
        firstName: string | null;
        lastName: string | null;
        addressId: string;
        addressType: string;
    }
    
    interface AddressWithIdYY {
        fName: string | null;
        lName: string | null;
        addressId: string;
        addressType: string;
    }
    
    function mapAddress3(address: AddressRegionXX | AddressRegionYY, region: string): AddressWithIdXX | AddressWithIdYY {
        let addressId = (<AddressRegionXX>address).id ? (<AddressRegionXX>address).id : "XX";
    
        let addressType = addressId == "XX" ? "subbed" : "unsubbed";
        switch (address.TypeDiscriminant) {
            case TypeDiscriminant.addressXX:
                return {
                    firstName: address.first_name || null,
                    lastName: address.last_name || null,
                    addressId: addressId,
                    addressType: addressType
                };
            case TypeDiscriminant.addressYY:
                return {
                    fName: address.f_name || null,
                    lName: address.l_name || null,
                    addressId: addressId,
                    addressType: addressType
                };
        }
    }
    // Usage
    let AXX: AddressRegionXX = { id: "idXX", TypeDiscriminant: TypeDiscriminant.addressXX };
    let resAXX = mapAddress3(AXX, "XX");
    
    let AYY: AddressRegionYY = { TypeDiscriminant: TypeDiscriminant.addressYY };
    let resAYY = mapAddress3(AYY, "YY");
    

    Хорошо, что mapAddress3 теперь знает точный тип address.

    Проблема в том, что resAXX и resAYY будут иметь тип AddressWithIdXX | AddressWithIdYY. Typescript не может знать, какой тип будет возвращен, пока не будет выполнено mapAddress3.

  3. Третий вариант может использовать условные типы для возврата правильного типа, подобного этому.

    function mapAddress4<T extends AddressRegionXX | AddressRegionYY>(address: T, region: string): T extends AddressRegionXX ? AddressWithIdXX : AddressWithIdYY;
    

    И ожидаемое использование будет

    var a4XX: AddressRegionXX = { id: "idAA" };
    var resa4XX: AddressWithIdXX = mapAddress4(a4XX, "XX");
    

    Но это невозможно. См. здесь

person Fyodor    schedule 07.05.2019
comment
Я попробовал вариант 3, но это вызывает другую проблему. Но то, что я пытался понять, было решением в соответствии с вариантом 3, который я читал в условных типах. - person codefreaK; 09.05.2019
comment
Я обновил вопрос с вашим ответом и его ошибкой - person codefreaK; 09.05.2019
comment
Вариант 3 невозможен. См. здесь github.com/Microsoft/TypeScript/issues/24929. - person Fyodor; 10.05.2019