Reputation: 10360
Full Playground here
https://www.typescriptlang.org/play?#code/C4TwDgpgB...
I am trying to create an object type that only allows specific keys, but for each key you must extend a specific generic type as value. I.e.
type AllowedKeys = "a" | "b" | "c";
type MyGenericType<T extends AllowedKeys> = {
type: T;
}
type MyObjectTypeGuard = {
[T in AllowedKeys]: MyGenericType<T>
}
// This is what I am aiming at, I tried to make sure that I now have an interface the should prevent me from using the only allowed keys with the allowed values
interface MyObject extends MyObjectTypeGuard {
a: { type: "a" } & { foo: "foo" };
b: { type: "b" } & { bar: "bar" };
c: { type: "c" } & { baz: "baz" };
}
Now that prevents me from using keys with the wrong values
// I cannot accidentially use the wrong base types
interface MyObject2 extends MyObjectTypeGuard {
a: { type: "b" } & { foo: "foo" };
b: { type: "b" } & { bar: "bar" };
c: { type: "c" } & { baz: "baz" };
}
// Or use completely wrong types even
interface MyObject3 extends MyObjectTypeGuard {
a: { foo: "foo" };
b: { type: "b" } & { bar: "bar" };
c: { type: "c" } & { baz: "baz" };
}
But I can still do other things that I want myself to prevent from
// But I can still miss a key
interface MyObject4 extends MyObjectTypeGuard {
a: { type: "a" } & { foo: "foo" };
b: { type: "b" } & { bar: "bar" };
}
// And I can still add arbitrary stuff
interface MyObject5 extends MyObjectTypeGuard {
a: { type: "a" } & { foo: "foo" };
b: { type: "b" } & { bar: "bar" };
c: { type: "c" } & { baz: "baz" };
fooBar: "baz";
}
I am looking for a way to define an object type / interface that forces me to use all keys (and only those) and has me extend a specific generic type for each value
We have a base model
BodyNode
which can be extended into variants:type BodyNode<T extends NodeType> = { readonly type: T; }; type NodeWithText<T extends NodeType> = BodyNode<T> & WithText; type NodeWithChildren<T extends NodeType> = BodyNode<T> & WithChildren; type NodeWithReference<T extends NodeType> = BodyNode<T> & WithReference; interface WithText { readonly text: string; } interface WithChildren { readonly children: Node[]; } interface WithReference { readonly href: string; } ``` to be able to iterate over the node types, we created an interface that maps `NodeType`s to variants types: ```typescript type BodyNodes = { [T in NodeType]: BodyNode<T>; }; interface Nodes extends BodyNodes { headline: NodeWithText; paragraph: NodeWithChildren; anchor: NodeWithReference; } type Node = Nodes[keyof Nodes]; ``` implementation: ```tsx type Components = { [K in BodyNode]: React.FC<{ node: Nodes[K] }>; }; const components: Components = { headline: Headline, paragraph: Paragraph, anchor: Anchor, }; const Body = (props: { nodes: Node[] }) => ( <div> {props.nodes.map(node => { const Component = components[node.type]; return <Component node={node} />; })} </div> ); ``` we are using the node types as keys for the `Nodes` as well as the `Components` and with this we can map the Nodes with the right types to the right components in type safe way. But this implementation has a big flaw: ```typescript type BodyNodes = { [T in NodeType]: BodyNode<T>; }; interface Nodes extends BodyNodes { headline: BodyNode<'headline'>; paragraph: BodyNode<'headline'>; // ^^^^^^^^ this will cause an error, which is what we want anchor: BodyNode<'anchor'>; yadda: 'foobar'; //^^^^^^^^^^^^^^^^ this is still possible because we can extend BodyNodes in // any way and that's not cool } ``` We'd love to find a way to write a type that allows only - specific keys as above - for specific keys only specific values as above - requires each key to be there - BUT forbids extending, ie. have an exact implementaition of that type and nothing else
Upvotes: 3
Views: 2862
Reputation: 9475
I don't think you can do this with interfaces: the 'extends' always means you can extend. However how about what's below? It's a little clunky since we're defining our new type inside <>, but I think it does what you want.
type AllowedKeys = "a" | "b" | "c";
type MyGenericType<T extends AllowedKeys> = {
type: T;
}
type MyObjectTypeGuard = { [T in AllowedKeys]: MyGenericType<T> };
type VerifyType<T extends MyObjectTypeGuard &
{ [U in Exclude<keyof T, AllowedKeys>]: never }> = T;
// Incorrect type letters or missing 'type', additional properties,
// and missing properties lead to errors
type VerifiedType = VerifyType<{
a: { type: "a" } & { foo: "foo" };
b: { type: "b" } & { bar: "bar" };
c: { type: "c" } & { baz: "baz" };
}>;
const obj: VerifiedType = {
a: { type: "a", foo: "foo" },
b: { type: "b", bar: "bar" },
c: { type: "c", baz: "baz" }
}
Upvotes: 2