V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
eraserking
V2EX  ›  TypeScript

想请教下 TypeScript 里面几个泛型方法的写法

  •  
  •   eraserking · 2022-08-10 14:29:12 +08:00 · 2025 次点击
    这是一个创建于 834 天前的主题,其中的信息可能已经有所发展或是发生改变。

    问题有点长,不过主要是有例子

    比如说我有这样的一个 interface

    interface MyInterface {
      propertyA: string | null;
      propertyB: number | null;
      propertyC: SomeOtherInterface | null;
      propertyD: AnotherOtherInterface | null;
    }
    
    interface SomeOtherInterface {
      propertyX: string | null;
      propertyY: number | null;
    }
    
    interface AnotherOtherInterface {
      propertyM: string | null;
      propertyN: number | null;
    }
    

    然后我需要一个方法去修改这个 MyInterface 的一个对象上的属性(假如说返回一个新对象而不是原地修改),那么我可以写出这样的一个泛型方法:

    const updateProperty = <
      TKey extends keyof MyInterface,
      TValue extends MyInterface[TKey]
    >(
      originalObject: MyInterface,
      propertyName: TKey,
      propertyValue: TValue
    ): MyInterface => {
      // ... some internal logic
      return { ...originalObject, [propertyName]: propertyValue };
    };
    
    

    到这里也没有问题,调用起来也很简单

    const new1 = updateProperty(original, "propertyA", "some string");
    

    第一个问题是,如果再写一个方法,一次性需要修改不确定数量的多个属性,这个泛型定义怎么写? 我想到的方法是,写个数组传

    const updateProperties = <
      TKey extends keyof MyInterface,
      TValue extends MyInterface[TKey]
    >(
      originalObject: MyInterface,
      propertiesToUpdate: { propertyName: TKey; propertyValue: TValue }[]
    ): MyInterface => {
      // ... some internal logic
      const updatedObject = { ...originalObject };
      propertiesToUpdate.forEach(
        (p) => (updatedObject[p.propertyName] = p.propertyValue)
      );
      return updatedObject;
    };
    

    设想的调用方法是

    const new2 = updateProperties(original, [
      {
        propertyName: "propertyA",
        propertyValue: "some string",
      },
      {
        propertyName: "propertyB",
        propertyValue: 10,
      },
    ]);
    

    但是实际上是不可以的,因为在修改多个不同类型的属性的时候,TKey 就是联合类型了,TValue 也会变成相对于 TKey 的联合类型,导致无法赋值回去 我想的是在 propertyName 为 A 的时候,propertyValue 只能为 string | null ,同时对于 B ,只能接收 number | null ,数组中各个元素是相互独立的 那我这个方法应该如何修改?

    第二个问题是,如果我想写一个方法去修改 propertyC -> propertyX / Y, propertyD -> propertyM/N ,这个泛型方法怎么写 期望的调用方法是

    updateSubProperty("propertyC", "propertyX", "some string");
    updateSubProperty("propertyD", "propertyY", 20);
    

    其中第一个参数限定为像 propertyC/D 这样的“对象”而不是 string number 这样的基础类型(描述的可能不准确),第二个第三个参数是限定为第一个参数确定下来的类型的属性和值 简单的想法如下

    const updateSubProperty = <
      TTop extends keyof MyInterface, // Should be restricted to some "object" only properties, but how?
      TSub extends keyof MyInterface[TTop],
      TValue extends keyof MyInterface[TTop][TSub]
    >(
      originalObject: MyInterface,
      topProperty: TTop,
      subProperty: TSub,
      value: TValue
    ) => {
      // ... some internal logic
      const originalSubObject = originalObject[topProperty] ?? ({} as TTop); // It's acceptable to leave some properties missing, no worry
      const newSubObject = { ...originalSubObject, [subProperty]: value }; // ERROR: Spread types may only be created from object types.ts(2698)
      return { ...originalObject, [topProperty]: newSubObject };
    };
    

    然后理所当然是报了错,Spread types may only be created from object types.ts(2698) 原因我也知道,TTop 那边应该限制为 object-only ,像上面那样写的话依然可以给第一个参数传 propertyA ,这不是我想要的。那么这边的这个泛型约束应该如何定义?

    6 条回复    2022-08-12 11:15:01 +08:00
    ifdef
        1
    ifdef  
       2022-08-10 14:38:48 +08:00
    Partial<MyInterface>
    exonuclease
        2
    exonuclease  
       2022-08-10 16:26:30 +08:00
    const updateProperties = (
    originalObject: MyInterface,
    newProperties: Partial<MyInterface>
    ): MyInterface => {
    return { ...originalObject,...newProperties };
    };
    YuJianrong
        3
    YuJianrong  
       2022-08-12 07:51:10 +08:00
    第一个你需要类似
    YuJianrong
        4
    YuJianrong  
       2022-08-12 07:53:55 +08:00
    第一个你需要类似这样的 helper 类型:
    type Property2<P> = P extends keyof MyInterface ? {propertyName: P; propertyValue: MyInterface[P]}: never;

    详见 https://www.typescriptlang.org/play?ssl=33&ssc=107&pln=33&pc=1#code/JYOwLgpgTgZghgYwgAgLIE8CS5rycgbwChlkAHKAezOjHQEEAuZAZzClAHNkAfZEAK4AbIQG4S5KjSh0AQs0EBbAEbRe-YWIkVqtdAGFmAZUqKIAeTAALaNkixEKPoJHjSO6XQAizeiErW0JY2UHa4juouWgC+RESg9ngoJmbBtjgO+MTuUnoAGsxsHCDczppukroy6ACaCgIqamWuRLHxGUnIfgEhaaEdEdmVnuiohexckeXaudUAcvWNUFMtbQD0a8gIlCBsyAJkACZwkAAKs3TIALzIADxEG6TIACoA0hDoyBAAHpAghyxkABrD6UGBoLADJAAGgem1IzwAanAhAIUD8-gCIWFMhAANpvD4AXThyAAfAAKUmkSgcTigFHmZQAKwgCDAzAwOKSsMeOSqdDmcDMzEJ6F58OGemRqIgoplaNJAEpOZDEhErmTCNTkI8AHQG1imFAJaAgFHIISUekIHVQCBgARQECEZAGvW04D081CJms9nQ5B4jx6IVmInMEPVBUoaLiR5xuKPba7MDIABuKIAjKruRrCNFkHBAXAQOh45sU3sQBAAO5Z677I4nCDnAXoCmZoRZwMAIijdHovb7LGNrAmJV7SvERDoNGQbZGACZbqctTdTl9fhB-oCQegwdioSgAPyEAfoMNyheiKXRlFo3PHvGnInRBQQdPQGcwAQgdnADsTbHGcFzABALBLhSnreoyLJshyR7qjCd5gOBLDMIuegrvuh5cseZJ4kSSoEG0VZpl2S5PshKA3KRRYlmW4jkfwdZLo2Bwga2YEQVBlGBniEhDPyIxXsw-YXAww4SCJ0oPtevajmY47FJw0mkNEsKkMJqGXsKCkXrI6lPBeMbMFmAAMWnIJpQkybpYnIBJ7b6MZsn3rKzAEBeBS9lmw66XUyBZrZGmwsRM5EEAA


    第二个更复杂点,我的做法是这样的 helper 类型:
    type NonNull<T> = T extends null ? never : T;
    type IsSubjectObjectKey<K extends keyof MyInterface> = NonNull<MyInterface[K]> extends object ? K : never;
    type GetSubjectObjectKey<K extends keyof MyInterface> = K extends IsSubjectObjectKey<K> ? K : never

    const updateSubProperty = <
    TTop extends GetSubjectObjectKey<keyof MyInterface>, // Should be restricted to some "object" only properties, but how?
    TSub extends keyof (NonNull<MyInterface[TTop]>),
    TValue extends NonNull<MyInterface[TTop]>[TSub]
    >(

    注意你的类型中含|null 需要去掉。

    详见 https://www.typescriptlang.org/play?ssl=26&ssc=3&pln=18&pc=1#code/JYOwLgpgTgZghgYwgAgLIE8CS5rycgbwChlkAHKAezOjHQEEAuZAZzClAHNkAfZEAK4AbIQG4S5KjSh0AQs0EBbAEbRe-YWIkVqtdAGFmAZUqKIAeTAALaNkixEKPoJHjSO6XQAizeiErW0JY2UHa4juouWgC+RESg9ngoJmbBtjgO+MTuUnoAGsxsHCDczppukroy6ACaCgIqamWuRLHxGUnIfgEhaaEdEdmVnuiohexckeXaudUAcvWNUFMtbXQ0yHOUIHOaADwAKgB8yAC8yAfIEAAekCAAJiwaIsgA-PwQAG5qzAfi6yhMCwjAJlAArCAIMDmcGQsAAaQg6D28KutwgDyeAGskZQYGgsAMkCdzlsdvsMGFMhAANrwgC6Jxud0eyEosKhb2QqIUX2g-3QGwA4hAwCCOdCJYjkajmRjWTj0HiCVSkiTuWiWU8geKIVCYXqEUiUSd3jyPt8oHEENs2MgBGR7nBIOKAAqzOhnZB7CQHA7UTXyp4isWgw0GuHSvaK5WUokQI4AGmQAHoU8gjFZKMJ7shVMgoBAisAoRBc2BKKxTCgAETsw01tkgIToYa0YBF5PKARgZBZgDur194sDmOQMfxAAoybsRHs44lHDS-dRGQBKRO+gBqcCEAhQcrHM4phMXSGX-rIjOX4vpRCOk4klA4nFAu4jUOYC-CSE3pArZDulUdC-Jef6sKCQEjL84rgZ8u77r8O57hARBrmcJzENE4hEDaIB2vBQhfqeP4oOcBDRMgcBPHAIDoOIDpOi6kEeugk6EcmNYeHo+g1px3HVHkfHIDWLDVqwEwlDWa4MY6zoQG6rHsbu-GsbxqnAbUwkAEwAAwyUQjHyYpmnKUIGkjF4wlcaxqDCQA5Hp9kyUAA
    YuJianrong
        5
    YuJianrong  
       2022-08-12 07:54:56 +08:00
    范型函数里面的报错一般都不重要,就不要管了,any 吧……
    eraserking
        6
    eraserking  
    OP
       2022-08-12 11:15:01 +08:00
    @ifdef
    @exonuclease
    确实 Partial 是可的,只是我的例子里还有些简化的地方,可能单独的 Partial 不能满足,我自己再先想想,谢谢

    @YuJianrong 对于问题一,这个看起来更好一点,我其实在那个数组里还有些别的属性,这样的话我也可以加进来了
    对于问题二,我得到了点启发,用了两个 helper type 去限定类型,然后用 NonNullable 再限定一下,这样的话就可以不用去掉 null
    参见:
    https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgLIE8CS5rycgbwChlkAHKAezOjHQEEAuZAZzClAHNkAfZEAK4AbIQG4S5KjSh0AQs0EBbAEbRe-YWIkVqtdAGFmAZUqKIAeTAALaNkixEKPoJHjSO6XQAizeiErW0JY2UHa4juouWgC+RESg9ngoJmbBtjgO+MTuUnoAGsxsHCDczppukroy6ACaCgIqamWuRLHxGUnIfgEhaaEdEdmVnuiohexckeXaudUAcvWNUFMtbXQ0yABiwEL2cxAAbtAAPAAqAHzIALyEEgDaANYQ6MigyE-olDDIp8hwLD9Hs8ALrICAAD0gIAAJgCQIc1AB+fgI5bMD7A5inIHoYHiaLiIjrFAAGQgcCO5mUACsIAgwOYQEJ0GdLjchjjXiB3s8vj9Mcg5pQQHNNHBlEIIGcccDLhCobDkJQaXSwMhkdiMcgFKjRMhWnFicgqbT6YzmazrlsdntUccyRSLCqzUyWRdznEiAhhWxkAIyNC4JAjAJlAAFWZ0K3HCSnU7UMGQiAwgEfPkm1XmlkYMKZCDnAA0yAA9MXkEYrJRhNDkKpkFAIEVgPSIDWwJRWKYUAAiZWmsDdpWu4a0YCNovKARqysAd0RsZDykTCtTvO+AAohSKxRKpRmXRacwMkOc7nHqLKAJQF2MANTgQgEKHlycVW9FInFkuO+4ZruOR6JI4p7nmQspnouwJEOc64SJQHCcKAD6-swgHhEgN6kO2ZARlUdBYvGZCYawoa4SMWKLsRBwPk+WL3o+EBEJeqFYMeKBXJcQzeiAvrwcAiEgA+i6-la658QJyHOmAdzYWReigoiyIENEl5-ACaF5mehF4hIDZgAIUDcgQAB0pniUhQi-kWMnUHJ1QCiZZkIRZwlSdZLCkZGuLMNRDHRLEBJxNxvq+SxuadOy0RqX8IDoOI-qBsGnl4eg66+UW3YeHoXjdhlWXVKguXIN24oIN2l6iEAA
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3417 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 36ms · UTC 10:50 · PVG 18:50 · LAX 02:50 · JFK 05:50
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.