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

2022-08-10 14:29:12 +08:00
 eraserking

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

比如说我有这样的一个 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 ,这不是我想要的。那么这边的这个泛型约束应该如何定义?

1771 次点击
所在节点    TypeScript
6 条回复
ifdef
2022-08-10 14:38:48 +08:00
Partial<MyInterface>
exonuclease
2022-08-10 16:26:30 +08:00
const updateProperties = (
originalObject: MyInterface,
newProperties: Partial<MyInterface>
): MyInterface => {
return { ...originalObject,...newProperties };
};
YuJianrong
2022-08-12 07:51:10 +08:00
第一个你需要类似
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
2022-08-12 07:54:56 +08:00
范型函数里面的报错一般都不重要,就不要管了,any 吧……
eraserking
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

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/871926

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX