【问题标题】:Accessing property in union of object types fails for properties not defined on all union members对于未在所有联合成员上定义的属性,访问对象类型联合中的属性失败
【发布时间】:2019-04-28 06:07:58
【问题描述】:

我面临的情况是 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
  };
 }
}

这是我必须使用联合类型的上下文 在这里,取决于每个区域地址类型的响应会发生变化,有一个很长的列表,在这里包含它是不切实际的。 正如我在这里展示的那样,每个地区的字段名称各不相同,并且还有一些额外的字段。 那么解决这种情况的优雅方法是使用条件类型。有没有联合类型的替代品。 与 ed 一样,至少有 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" });

【问题讨论】:

  • 只用type guard?
  • 请编辑此问题以提供您的问题的minimal reproducible example,以便有人可以提出直接解决问题的建议。否则,任何答案都只是对您所面临的实际问题的猜测。
  • @jcalz 你现在可以看看吗
  • 我很想看看这个,但进入门槛仍然太高。为了使该代码甚至可以无错误地编译以开始处理答案,有人需要为AddressRegion等类型发明接口/类型定义。你能做到吗?理想情况下,您会给我们一些代码,有人可以将它们原样放入 IDE(如the Playground)并开始进行修改和建议。唯一的错误应该是与问题相关的错误。这是minimal reproducible example 的“完整”和“可验证”部分。祝你好运!
  • 理想情况下,Q 和 A 应该是这样的:“Q:这里有一些代码很乏味,而且无法扩展。有没有更好的方法来做到这一点?A:这里有一些更改的代码具有相同的效果,但不那么乏味且可扩展性更好。”我要求你做 Q 部分。如果您的示例代码充满错误,那么要么我必须自己修复它们,要么我必须接受答案代码中的错误,这意味着我无法测试答案,这意味着它可能无法正常工作。我个人对建议一个我无法测试的答案感到不舒服。也许会有其他感觉不同的人出现。

标签: typescript union-types


【解决方案1】:

为什么这是“最佳”方式的完整解释(我开玩笑说没有最好的方式)可能超出了堆栈溢出答案的范围,但基本上有几点是:

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

  2. 使用 'Maybe' 可以有两种可能的结果,这可以作为 undefined/null 的解决方案,但是在这种情况下,它解决了从联合中选择时你可能有 'Something' 这将是 typeA 或typeB 或者什么都没有,这是 case typeC

  3. 有条件的返回类型通常比重载更糟糕,因为一旦你有了一个有条件的返回类型,你几乎是在判自己必须以某种方式强制转换返回值。
  4. 阅读更多关于 Scala 的“选项”或 Haskells 的“可能”,其中此解决方案基于其他地方,它是否超出了此答案的范围,但此解决方案基本上窃取了它的最佳想法。

长代码 sn-p 传入。希望这会有所帮助,但事实并非如此。

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 是 monadic 和 monads 陷阱效果,Maybe 陷阱效果是非确定性的,这里的想法是 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

【讨论】:

  • Shanon 我对你的回答很感兴趣,但老实说我在这里不够深入。我正试图围绕你的答案。如果您可以包装添加评论来解释相同的内容,那就太棒了。如果它不是太多要求
  • 这很难解释,但我已经尽力了,基本的答案是,在做非确定性,例如在联合成员属性不具有该属性时访问它们,例如使用可以为空的值 |未定义
  • 是的,已经拿到了那部分。我正在考虑建立一个也许我自己。所以我自己可以理解。我一直在考虑使用交集的相同思路,这意味着所有属性都可以访问。但实际上这是不对的,因为它应该是 TypeIA 或 TypeIB 或 TypeIC 。但也许它会很好地解决这个难题
【解决方案2】:

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

  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");
    

    这里的重点是根据农业类型获得确切类型的结果。对于AddressRegionXX 类型的address,您将得到AddressWithIdXX 类型的结果。

    这种方法的问题是在实现mapAddress 函数时应该非常小心。它里面的代码不知道address 的类型,所以你应该依赖一些额外的条件。 (Typescript 代码将被转译为 JavaScript,所有接口都将被移除)。

  2. 使用discriminated unions

    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 的确切类型。

    问题是resAXXresAYY 的类型为AddressWithIdXX | AddressWithIdYY。在执行mapAddress3 之前,Typescript 无法知道将返回什么类型。

  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");
    

    但这是不可能的。见here

【讨论】:

  • 我已经尝试了 option3 但这会导致另一个问题。但我试图想出的是一个解决方案,类似于我正在阅读条件类型的选项 3
  • 我已经用您的答案及其错误更新了问题
  • 选项 3 是不可能的。请看这里github.com/Microsoft/TypeScript/issues/24929
猜你喜欢
  • 2019-10-06
  • 1970-01-01
  • 2020-09-27
  • 2015-01-09
  • 2022-10-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-04-04
相关资源
最近更新 更多