I have a static object definition that I would like to ensure conforms to a type but also be able to use the constant definition for inferred types. For example:
const TREE_BRANCH_TYPES = ['Type1', 'Type2'] as const;
type TreeBranchType = typeof TREE_BRANCH_TYPES[number];
type TreeRoot = Record<TreeBranchType, TreeBranch>;
type TreeBranch = { leaves: readonly TreeLeaf[] };
type TreeLeaf = { name: string };
const tree: TreeRoot = {
Type1: {
leaves: [
{ name: 'Type1-Leaf1' },
{ name: 'Type1-Leaf2' },
]
},
Type2: {
leaves: [
{ name: 'Type2-Leaf1' },
{ name: 'Type2-Leaf2' },
]
}
} as const;
type TreeLeafName = typeof tree[TreeBranchType]['leaves'][number]['name'];
function findLeaf(type: TreeBranchType, name: TreeLeafName): TreeLeaf | undefined {
return tree[type].leaves.find(leaf => leaf.name === name);
}
Given the above, the compiler will require tree
to conform to the TreeRoot
definition, but TreeLeafName
is interpreted by the compiler as type TreeLeafName = string
.
However, if I remove the type from tree
(i.e. const tree = { ... };
), then TreeLeafName
is recognized by the compiler as type TreeLeafName = "Type1-Leaf1" | "Type1-Leaf2" | "Type2-Leaf1" | "Type2-Leaf2"
.
Is there a way to get the best of both worlds? Type safety on the tree
constant definition and type safety of the inferred TreeLeafName
type?
CodePudding user response:
The easiest way to do this with TS4.9 and above is to use the satisfies
operator to check that a value is assignable to a type without widening/upcasting it to that type:
const tree = {
Type1: {
leaves: [
{ name: 'Type1-Leaf1' },
{ name: 'Type1-Leaf2' },
]
},
Type2: {
leaves: [
{ name: 'Type2-Leaf1' },
{ name: 'Type2-Leaf2' },
]
}
} as const satisfies TreeRoot;
type TreeLeafName = typeof tree[TreeBranchType]['leaves'][number]['name'];
// type TreeLeafName = "Type1-Leaf1" | "Type1-Leaf2" | "Type2-Leaf1" | "Type2-Leaf2"