B R*_*Rob 6 typescript typescript-generics
我们想要获取一个文档类型并改变它的一些字段,然后返回具有新类型的新文档。我们想根据一组值(collectionDataFields在下面的示例中)来确定要改变的字段。
type AllDocuments = {
__typename: 'cat'
a: '123',
b: '123',
z: 'xyz'
} | {
__typename: 'dog'
a: '123',
c: '123',
z: 'xyz'
}
const collectionDateFields = {
cat: ['a', 'b'],
dog: ['a', 'c']
} as const
export const useStringsToNums = (document: AllDocuments) => {
const dateFields = collectionDateFields[document.__typename]
dateFields.forEach((field) => {
document[field] = parseInt(document[field])
})
return document
}
Run Code Online (Sandbox Code Playgroud)
在示例中,dateFields输入为,readonly ["a", "b"] | readonly ["a", "c"]但个人输入field为any。
在document返回的AllDocuments,虽然我们希望它有改变的类型的字段(如一个连number)。
编辑:对于用法的一个例子,我会做这样的事情:
useStringsToNums({
__typename: 'cat'
a: '123',
b: '123',
z: 'xyz'
})
Run Code Online (Sandbox Code Playgroud)
在这种情况下,该函数将匹配的属性__typename: 'cat'转换为数字(带有parseInt)。
我们想要采用文档类型并改变它的一些字段
如果您有一个类型的对象,其中string某个键上有一个 -valued 属性,则您无法将 a 分配number给该相同的属性,而编译器不会抱怨。TypeScript 背后的动机之一是为 JavaScript 添加一个强大的静态类型系统,而将 a 分配number给应该是 a 的东西则string违背了这一点:
function doSomething(doc: AllDocuments) {
doc.a = 123; // error! Type '123' is not assignable to type '"123"'
}
Run Code Online (Sandbox Code Playgroud)
您可以使用类型断言来抑制这些错误:
function doSomething(doc: AllDocuments) {
doc.a = 123; // error! Type '123' is not assignable to type '"123"'
}
Run Code Online (Sandbox Code Playgroud)
但从长远来看,这一切只会让事情变得更糟。TypeScript 不会模拟任意类型突变,只会缩小范围。无法说在调用 后doSomething(doc), 的类型doc已更改,因此以前的string属性现在是number。通过断言 anumber是 a string,您对编译器撒了谎,并且很容易在运行时遇到错误:
const doc: AllDocuments = { __typename: "cat", a: "123", b: "123" };
doSomething(doc);
try {
doc.a.toUpperCase(); // compiles but has runtime error:
} catch (e) {
console.log(e); // doc.a.toUpperCase is not a function
}
Run Code Online (Sandbox Code Playgroud)
假设您不想将这些属性类型扩展到"123"可以同时处理"123"和 的123类型,那么最好创建一个您正在寻找的类型的新对象,并将原始对象视为不可变的:
function doSomethingElse(doc: AllDocuments) {
return { ...doc, a: 123 }
}
const newDoc = doSomethingElse(doc);
/* const newDoc: {
a: number;
__typename: 'cat';
b: '123';
} | {
a: number;
__typename: 'dog';
c: '123';
} */
console.log(newDoc.a.toFixed(2)); // "123.00"
Run Code Online (Sandbox Code Playgroud)
对于这个答案的其余部分,我将使用这种方法。
现在回答你剩下的问题。您遇到了我所说的相关联合类型的问题,如microsoft/TypeScript#30581中所述。
在下面的代码中:
const dateFields = collectionDateFields[document.__typename]
dateFields.forEach((field: "a" | "b" | "c") => {
document[field] // oops
})
Run Code Online (Sandbox Code Playgroud)
即使我们用field我们期望的类型进行注释,编译器也无法看到它document有一个名为 的键field。的类型document是两个不同类型的并集,并且 的类型dateField也是两个不同类型的并集,并且 的类型field也是并集。但编译器将这三个联合视为独立或不相关的。据其了解,document.__typename可能是"cat",但也field可能是"c"。 你知道这是不可能的,但是编译器丢失了线程。
有时可以重构代码以避免相关联合问题。不幸的是,尽管花了一些时间尝试从编译器中获取某种类型的安全类型,但我能得到的最接近的类型还不够有用,不值得这么复杂:
const useDatesToLuxon = <T extends AllDocuments['__typename']>(
document: Extract<AllDocuments, { __typename: T }>
) => {
type Fields = (typeof collectionDateFields)[T][number];
const dateFields: readonly Fields[] = collectionDateFields[document.__typename]
const doc = document as Record<Fields, string>;
const newDoc = {} as Record<Fields, number>;
dateFields.forEach((field) => {
newDoc[field] = parseInt(doc[field])
})
const ret = { ...(document as Omit<typeof document, Fields>), ...newDoc };
type Ret = typeof ret;
return ret as { [K in keyof Ret]: Ret[K] }
}
Run Code Online (Sandbox Code Playgroud)
这是很多类型杂耍,最终它只适用于已知为联合的单个成员的参数AllDocuments:
const r = useDatesToLuxon({ __typename: "cat", a: "123", b: "123" });
/* const r: {
__typename: 'cat';
a: number;
b: number;
} */
console.log(r); // as expected
Run Code Online (Sandbox Code Playgroud)
如果您尝试在其类型仅已知为完整AllDocuments联合的对象上调用它,您会得到错误的输出类型:
function butWait(doc: AllDocuments) {
const u = useDatesToLuxon(doc);
/* const u: {
__typename: "cat" | "dog";
a: number;
b: number;
c: number;
} */
// UGH, no, that's not the type we want; we cannot programmatically *distribute* the
// behavior of useDatesToLuxon across the union members of AllDocuments
}
Run Code Online (Sandbox Code Playgroud)
所以让我们放弃这样的重构。
对于关联联合,没有一个很好的解决方案。解决该问题的一种方法是使用冗余代码,将代码块分成几个相同的块,编译器可以在其中使用控制流分析来遍历不同的情况:
export const useDatesToLuxon = (document: AllDocuments) => {
switch (document.__typename) {
case "cat": {
const dateFields = collectionDateFields[document.__typename]
const temp = {} as Record<typeof dateFields[number], number>
dateFields.forEach((field) => {
temp[field] = parseInt(document[field])
})
return { ...document, ...temp }
}
case "dog": {
const dateFields = collectionDateFields[document.__typename]
const temp = {} as Record<typeof dateFields[number], number>
dateFields.forEach((field) => {
temp[field] = parseInt(document[field])
})
return { ...document, ...temp }
}
}
}
Run Code Online (Sandbox Code Playgroud)
这工作得很好,而且类型非常安全。在这种"cat"情况下"dog",编译器确切地知道发生了什么。但这种冗余无法扩展。如果您可以要求编译器假装您编写了此类冗余代码,就像我在microsoft/TypeScript#25051中所要求的那样,那就太好了,但您不能。
对于这种情况,我的一般建议是使用类型断言来放松函数实现内部的类型检查。这将维护类型安全的负担从编译器(这里无法正确完成)转移到了您身上,因此您需要小心。
这是一种可能的方法。首先,让我们计算我们期望从函数中得出的类型:
type CollectionDateFields = typeof collectionDateFields;
type ConvertedDocuments = {
[D in AllDocuments as D['__typename']]: {
[K in keyof D]: K extends
CollectionDateFields[D['__typename']][number] ? number : D[K]
} }[keyof CollectionDateFields]
Run Code Online (Sandbox Code Playgroud)
这是使用键重新映射来迭代 的每个成员并将其中AllDocuments提到的相关属性转换为。您可以验证它是否正确:collectionDateFieldsnumber
/* type ConvertedDocuments = {
__typename: "cat";
a: number;
b: number;
} | {
__typename: "dog";
a: number;
c: number;
} */
Run Code Online (Sandbox Code Playgroud)
现在我们可以给出useDatesToLuxon一个重载调用签名来描述它的作用:
function useDatesToLuxon<D extends AllDocuments>(
document: D
): Extract<ConvertedDocuments, Pick<D, '__typename'>>
Run Code Online (Sandbox Code Playgroud)
这是一个通用函数,可以将整个AllDocuments联合转换为整个ConvertedDocuments联合,以及将任何子类型转换为类似的输出类型。现在来实施:
function useDatesToLuxon(_document: AllDocuments): ConvertedDocuments {
// let's just pretend it's one of the members of the union
const document = _document as Extract<AllDocuments, { __typename: "cat" }>
const dateFields = collectionDateFields[document.__typename]
const temp = {} as Record<typeof dateFields[number], number>
dateFields.forEach((field) => {
temp[field] = parseInt(document[field])
})
return { ...document, ...temp }
}
Run Code Online (Sandbox Code Playgroud)
在这里,我最容易做出的断言就是假装是document工会成员之一。这在技术上并不正确,但它抑制了编译器的抱怨。我们只需要小心我们是否正确实施了它。
现在来测试一下:
const r = useDatesToLuxon({ __typename: "cat", a: "123", b: "123" });
/* const r: {
__typename: 'cat';
a: number;
b: number;
} */
console.log(r); // as expected
function andThen(doc: AllDocuments) {
const u = useDatesToLuxon(doc);
/* {
__typename: "cat";
a: number;
b: number;
} | {
__typename: "dog";
a: number;
c: number;
} */
}
Run Code Online (Sandbox Code Playgroud)
看起来不错!
| 归档时间: |
|
| 查看次数: |
143 次 |
| 最近记录: |