3

Updated Version

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

Old version

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
2
  • 2
    Your first definition of Nodes does not compile because it mentions NodeWithText, NodeWithChildren, and NodeWithReference which are generic types that need type parameters. Could you try to make sure your code constitutes a minimal reproducible example suitable for dropping into a standalone IDE like The TypeScript Playground? The minimum example probably has less code also: if this is what you're looking for, for example Commented Nov 5, 2020 at 22:03
  • Hey @jcalz thanks, silly me, always do the same mistakes. I completely updated my question and added a link to a ts playground at the top Commented Nov 9, 2020 at 19:57

1 Answer 1

3

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" }
}
Sign up to request clarification or add additional context in comments.

1 Comment

Perfect! Thank you so much!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.