[英]Is there a way to define a typescript type which recursively narrows the type of all descendents
我有一堆 typescript 类型,它们使用共享的Inaccessible
接口代替用户无权访问的对象。
但是,通常我使用的类型比上面的类型(来自 GQL 端点的嵌套数据)更复杂,并且我正在编写的组件只有在定义了整个 object 时才能工作。 因为这很常见,所以我希望能够编写一个类型保护器,我可以在任何组件中使用它,并递归地从基类型的所有后代中“删除”所有Inaccessible
类型。
这是我正在尝试做的一个例子。 但至关重要的是, AllPropertiesAccessible<T>
类型将是通用的,因此它可以与具有 Inaccessible 后代的其他类型一起使用。
interface Inaccessible {
invalidId: string;
}
interface Person {
id: string;
name: string;
}
interface Node {
children: Array<Node | Inaccessible>;
user: Person | Inaccessible;
spouse: Person | Inaccessible;
}
interface FamilyTree {
rootNode: Node | Inaccessible;
}
function isFamilyTreeAccessible(familyTree: FamilyTree) familyTree is AllPropertiesAccessible<FamilyTree> {
// logic which recursively validates that all elements in the tree are accessible
}
function FamilyTreeOrInaccessibleDisplay({familyTree}: {familyTree: FamilyTree}): JSX.Element {
if (isFamilyTreeAccessible(familyTree)) {
return <FamilyTreeDisplay familyTree={familyTree} />
} else {
return <NotAccessibleMessage />
}
}
function FamilyTreeDisplay({familyTree}: {familyTree: AllPropertiesAccessible<FamilyTree>}) {}
function NotAccessibleMessage() {
return <div>Inaccessible</div>
在这个例子中,我们有一个FamilyTree
类型,它有一堆可能无法访问的后代。 AllPropertiesAccessible<FamilyTree>
类型等同于:
interface AccessibleNode {
children: Array<AccessibleNode>;
user: Person;
spouse: Person;
}
interface AccessibleFamilyTree {
rootNode: AccessibleNode;
}
有没有办法在typescript中写出这样一个AllPropertiesAccessible
类型?
鉴于您的示例代码,我倾向于将AllPropertiesAccessible
定义为以下递归条件类型:
type AllPropertiesAccessible<T> =
T extends Inaccessible ? never :
{ [K in keyof T]: AllPropertiesAccessible<T[K]> }
它是一种分配条件类型,因此它将T
拆分为其联合成员; 它们中的任何一个Inaccessible
都将被删除(因为它们评估为不可能的never
type ),而那些未被删除的将被映射,因此任何属性本身都会使用AllPropertiesAccessible<>
进行转换。
这个定义看似简单,因为你可能想知道如果T
是像string
这样的原始类型(是否映射了它的所有明显属性,如length
?)或者如果T
是一个数组(是否映射了它的所有数组方法和属性,如length
)会发生什么?)。 幸运的是,由于{[K in keyof T]: AllPropertiesAccessible<T[K]>}
是同态映射类型(请参阅“同态映射类型”是什么意思? ),它会自动单独保留原语(因此AllPropertiesAccessible<string>
将是string
) 并自动将数组/元组映射到数组/元组) (因此AllPropertiesAccessible<T[]>
将与AllPropertiesAccessible<T>[]
相同)。
让我们测试一下:
type AccessibleFamilyTree = AllPropertiesAccessible<FamilyTree>;
/* type AccessibleFamilyTree = {
rootNode: {
children: ...[];
user: {
id: string;
name: string;
};
spouse: {
id: string;
name: string;
};
};
} */
这看起来不错,虽然rootNode
的children
属性的类型显示有点混乱。 ...[]
是什么意思?
好吧,问题是因为FamilyTree
是递归类型, AccessibleFamilyTree
也是递归的。 但是AllPropertiesAccessible<T>
类型不会为转换生成的中间类型赋予新名称。 事实上,如果你自己写出预期的类型,你会发现自己正在寻找一个新的命名类型:
interface AccessibleFamilyTreeManual {
rootNode: AccessibleNode;
}
// need to name this
interface AccessibleNode {
children: Array<AccessibleNode>;
user: Person;
spouse: Person;
}
如果你试图让AccessibleNode
保持匿名,你最终会无限倒退。 编译器通过在显示类型时放弃来处理这个问题。 它仍然知道它是什么:
declare let aft: AccessibleFamilyTree;
const foo = aft.rootNode.children.map(x => x.user.name)
// ---------------------------------> ^
// (parameter) x: {
// children: ...[];
// user: { id: string; name: string; };
// spouse: { id: string; name: string; };
// }
// const foo: string[]
aft.rootNode.children
的每个元素的类型都是已知的,它们具有某种递归数组类型的children
元素,以及正确形状的user
和spouse
属性。
为了确保AccessibleFamilyTree
和AccessibleFamilyTreeManual
被认为是等价的,让我们在它们之间做一些分配:
declare let aftm: AccessibleFamilyTreeManual;
aft = aftm; // okay
aftm = aft; // okay
这些赋值成功的表面表明编译器认为它们的类型可以根据需要相互赋值。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.