0

I want to limit the key of an object with interface.

I can achieve this by using type

type KeyOfObj = 'a' | 'b' | 'c';

type Type = { [key in KeyOfObj]?: string };

const objWithType: Type = {
  a: 'a', // valid
  b: 'b', // valid
  c: 'c', // valid
  d: 'd', // invalid
};

Is there a way to achieve the above behavior using interface? In other words, is there a way to make code below valid?

type KeyOfObj = 'a' | 'b' | 'c';

interface Interface {
  [key in KeyOfObj]?: string; // this code throws error
}

const objWithInterface: Interface = {
  a: 'a', // valid
  b: 'b', // valid
  c: 'c', // valid
  d: 'd', // invalid
};

I am still confused about when to use type vs interface for object even after reading this (https://medium.com/@martin_hotell/interface-vs-type-alias-in-typescript-2-7-2a8f1777af4c) article.

I always thought interface could do more than what type could do in terms of object. However, for the case above, it seems like type can do more compared to what interface can do. Any advice?

-- Question Summary --

  1. How to make the second block of code valid while achieving what the first block of code is doing?

  2. Any advice when to use type vs interface dealing with object?

Thanks!

1 Answer 1

1

Type aliases and interfaces are different in subtle ways that are hard to list out exhaustively. I tend to think of interfaces as being easier to use but harder to define than type aliases. Since you're trying to define an interface, let's look at the rules for defining them.

Type aliases can give a name to just about any type, and as such are very flexible in how you're allowed to define them. The type {[key in KeyOfObj]?: string} is a mapped type. That's a type, so you can give it a name via type alias:

type Type = { [key in KeyOfObj]?: string };

Interfaces can only be defined by explicitly writing out key/value pairs and by using extends to extend named types whose property keys are known statically. A mapped type using the {[A in B]: C} syntax cannot be used directly as the definition of an interface, since it is neither an explicitly written-out list of key/value pairs, nor is it a named type.

But, you do have a type named Type, and when you inspect it, you see that the compiler knows that its keys are "a", "b", and "c":

/* type Type = {
    a?: string | undefined;
    b?: string | undefined;
    c?: string | undefined;
} */

Thus the type Type is a valid target for extends, and you can define your interface like this:

interface Interface extends Type {} // okay

So while the mapped type in question cannot be used directly in an interface definition, it can be used indirectly by being given a name.

This works as you wanted:

const objWithInterface: Interface = {
  a: "a", // valid
  b: "b", // valid
  c: "c", // valid
  d: "d" // invalid
};

Do note that you can't always use this trick; it depends on whether or not the type you're extending has statically known keys. Here's a way it can fail:

type TypeWithUnknownKeys = { a: string, b: string } | { c: string };

interface InterfaceFailure extends TypeWithUnknownKeys {}; // error!
//                                 ~~~~~~~~~~~~~~~~~~~
// An interface can only extend an object type (or 
// intersection of object types) with statically known members.

This fails because a union type doesn't have statically known keys. The type in question will either have an a key and a b key or a c key, but the compiler can't think of this as a single object type to extend. So even though it has a name, you can't use it to build an interface.

Okay, hope that helps. Good luck!

Link to code

Sign up to request clarification or add additional context in comments.

1 Comment

Yes, this is the exact answer I was looking for. Thank you for such a clear explanation! jcalz

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.