Не удается получить доступ к свойству в объединении типов объектов для свойств, определенных не для всех членов объединения - PullRequest
0 голосов
/ 28 апреля 2019

Я сталкиваюсь со сценарием, в котором ответ 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
  };
 }
}

Это контекстгде я должен использовать тип объединения. Здесь будет меняться ответ в зависимости от типа адреса каждого региона, имеется длинный список, который нецелесообразно включать здесь.Как я показал здесь, имена полей различаются для каждого региона и для некоторых дополнительных полей есть.Итак, что является элегантным способом решения этой ситуации, так это уместно использовать условные типы.Есть ли альтернатива союзному типу?Как и в 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 (address: 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"
  }
}

РЕДАКТИРОВАТЬ: - Применение условных типов, но использование универсальных приводит к другой ошибке 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" });

Ответы [ 2 ]

1 голос
/ 10 мая 2019

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

  1. чтобы получить ключ объединения, вам нужно превратить его в пересечение ключей (A | B) с ключами (A & B)

  2. Использование «Возможно», при котором вы можете иметь два возможных результата, удваивается как решение для неопределенного / нулевого, однако в этом случае это решает, что при выборе из объединения вы можете получить «Что-то», которое будет типа А или typeB или ничего типа caseC

  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

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

Суть в этом можно легко упустить, потому что любой другой переполнитель будет утверждать, что это можно легко решить, вложив 'If / Else', но идея 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
1 голос
/ 07 мая 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");
    

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

...