简体   繁体   中英

How to create a generic TypeScript interface which matches any type/interface/object with restrictions on types for values inside it?

I want to create a generic TypeScript interface which matches any type or interface or object with restrictions on types for values inside it.

Here's MyInterface which has properties fooIProp and barIProp which stores strings with an example:

interface MyInterface {
  fooIProp: string;
  barIProp: string;
};

const testInterface: MyInterface = {
  fooIProp: "foo",
  barIProp:  "bar"
};

Here's MyType type alias which has properties fooTProp and barTProp which stores strings with an example:

type MyType = {
  fooTProp: string;
  barTProp: string;
}

const testType: MyType = {
  fooTProp: "foo",
  barTProp: "bar"
}

Here's an object which has properties fooObjectKey and barObjectKey which stores strings:

const testObject = {
  fooObjectKey: "foo",
  barObjectKey: "bar"
}

I create MyGenericInterface which accepts object with strings as keys and values as follows:

interface MyGenericInterface { [key: string]: string }

Then I try to assign testInterface to MyGenericInterface

const testFromInterface: MyGenericInterface = testInterface;
const testFromType: MyGenericInterface = testType;
const testFromObject: MyGenericInterface = testObject;

It throws TS2322 error:

Type 'MyInterface' is not assignable to type 'MyGenericInterface'.
  Index signature is missing in type 'MyInterface'.(2322)

Here's TypeScript Playground for reference.

Question : How can I create a generic TypeScript interface which matches any type/interface/object with restrictions on types for values inside it?

It is a known issue (and currently working as intended) that implicit index signatures are not added to values of an interface type, whereas they are added for anonymous object types. See microsoft/TypeScript#15300 for discussion.


Technically speaking it is not safe to add implicit index signatures this way. Object types in TypeScript are open/extendible and not closed/exact (see microsoft/TypeScript#12936 for discussion/request for exact types), so it's always possible to have properties in addition to the known properties mentioned in the interface:

const someObject = {
  fooIProp: "foo",
  barIProp: "bar",
  baz: 12345
};
const unexpectedMyInterface: MyInterface = someObject; // no error

(although if you try to add such unknown properties directly in an annotated object literal you will get excess property warnings, which sometimes makes object types in TypeScript seem exact when they are not).

The fact that unexpectedMyInterface is assignable to MyInterface but not to MyGenericInterface is why it is technically correct to prohibit the assignment. Of course you can go through the same exercise with MyType :

const someOtherObject = {
  fooTProp: "foo",
  barTProp: "bar",
  baz: 12345
};
const unexpectedMyType: MyType = someOtherObject;

...yet the bad assignment of unexpectedMyType to MyGenericInterface is allowed :

const badMyGenericInterface: MyGenericInterface = unexpectedMyType;
console.log(badMyGenericInterface.baz?.toUpperCase()); // no error in TS, but 
// RUNTIME 💥 badMyGenericInterface.baz.toUpperCase is not a function

So, what gives? It seems to be a tradeoff. It is considered "safer" to allow this for anonymous object types and not interface types because interface types can be augmented or merged later, while anonymous object types can't be.

Personally I don't see it as much safer for one than the other. If you want to see implicit index signatures apply to interface types also, then you might want to go to microsoft/TypeScript#15300 and give it a.


Until and unless that feature gets implemented, all you can have is workarounds. One is to use a mapped type to convert a value of MyInterface to an object type equivalent before assigning it to MyGenericInterface :

type Id<T> = { [K in keyof T]: T[K] } // change interface to mapped type version
type MyInterfaceAsType = Id<MyInterface>;
const testFromInterface2: MyGenericInterface = testInterface as MyInterfaceAsType
const testInterfaceAnonymousTypeVersion: MyInterfaceAsType = testInterface;
const testFromInterface3: MyGenericInterface = testInterfaceAnonymousTypeVersion;

There may be other workarounds but I'd need to see more of a use case before spending too much time suggesting them. Where are you seeing failures you'd like to fix?


Playground link to code

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM