对于未在所有联合成员上定义的属性,无法访问对象类型的联合中的属性

时间:2019-04-28 06:07:59

标签: typescript union-types

我正面临一个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类型的上下文 在这里,取决于每个区域地址类型的响应将改变,这里有一个很长的列表,在这里不切实际。 正如我在此处所示,字段名称因每个区域而异,并且还有一些其他字段。 因此,解决这种情况的理想方法是使用条件类型是适当的。联合类型是否有其他选择。 与enned一样,至少会有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,区域:字符串):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"
  }
}

编辑:-应用条件类型但使用通用类型会导致另一个皮棉错误,如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 个答案:

答案 0 :(得分:1)

我看到以下选项(您可以将它们组合在一起):

  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的{​​{1}},您将得到类型为AddressRegionXX的结果。

    此方法的问题是,在实现AddressWithIdXX函数时应格外小心。其中的代码不了解mapAddress的类型,因此您应该在一些附加条件下进行中继。 (Typescript代码将被转换为JavaScript,所有接口都将被删除。)

  2. 使用discriminated unions

    address

    这里的好处是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的确切类型。

    问题是addressresAXX的类型为resAYY。在执行AddressWithIdXX | AddressWithIdYY之前,Typescript不知道将返回哪种类型。

  3. 第三种选项可以使用条件类型返回正确的类型

    mapAddress3

    预期使用量将是

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

    但这是不可能的。参见here

答案 1 :(得分:1)

关于为什么这是“最佳”方法的完整解释(我开玩笑说,没有最佳方法)可能超出了堆栈溢出答案的范围,但从本质上讲,以下几点是简单的:

  1. 要完全获取联合类型的关键字,您需要将其转换为 (A | B)与keyof(A&B)的交集

  2. 使用“也许”(您可能在其中有两个可能的结果)加倍作为未定义/空值的解决方案,但是在这种情况下,它解决了从联合中选择时您可能会有“某物”(其类型为A或typeB或不是大小写为C的

  3. 有条件的ReturnType通常比重载更糟糕,这仅仅是因为第二种情况是您有条件的返回类型,您几乎已经判刑自己必须以某种方式转换返回值。
  4. 了解有关Scala的“选项”或Haskells“也许”的更多信息,该解决方案基于其他地方,是否超出了此答案的范围,但该解决方案基本上窃取了其最佳想法。

长代码段传入。希望这有助于感觉过度,但事实并非如此。

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

编辑:“了解更多”: 也许并不是要为您解决这一一次性问题,而是要解决编程中可能发生的某种“效应”。也许是monadic,而monads会捕获效果,可能是traps是非确定性的,这里的想法是Maybe可以将其抽象化,因此您不必考虑它。

这里的观点很容易被遗漏,因为其他任何溢出者都声称可以通过嵌套“ 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