Home > Software engineering >  How to constrain a TypeScript interface to have only string property values?
How to constrain a TypeScript interface to have only string property values?

Time:10-07

I have a function X that accepts Record<string, string>:

function X(arg:Record<string, string>) {
 ...
}

I want to define a function Y that is generic in T that calls X:

function Y<T>(arg:T) {
 ...
 X(arg)
 ...
}

I want to instantiate T with interfaces that have string properties like:

interface UserInfo {
 name: string;
 email: string;
}

Y<UserInfo>({ name: 'Jon', email: '[email protected]' });

But I cannot define Y because T is not constrained to Record<string, string>. If I write:

function Y<T extends Record<string, string>>(arg:T) {
 ...
 X(arg)
 ...
}

Then I cannot call Y with UserInfo.

How do I define Y?

Thank you!

CodePudding user response:

You're running into the situation reported in microsoft/TypeScript#15300. The type Record<string, string> is equivalent to {[k: string]: string}, a type with a string index signature.

TypeScript will give anonymous object types (like the type {name: string, email: string}) implicit index signatures to allow you to assign them to types with index signatures, as long as the known properties don't conflict. But it does not give implicit index signatures to structurally equivalent interface types. This is one of the few places where you can observe a difference in behavior between interfaces and anonymous object types (or type aliases of such types).

The reasoning given in microsoft/TypeScript#15300 is that it is "safer" to allow implicit index signature for type aliases of anonymous object types than it is to do so for interfaces, because you can merge new members into existing interfaces while you cannot do so for anonymous object types. I'm not really sure I understand the motiviation behing this reasoning... but for now, anyway, that's just how TypeScript works.


The fix here for your example code, which is already generic is to replace the index signature constraint T extends Record<string, string> with a recursive constraint T extends Record<keyof T, string>. We can't be sure that T has an index signature, but we can be sure that it has its own keys:

function Y<T extends Record<keyof T, string>>(arg: T) { 
    X(arg) // this still works
}

And the implementation works because the generic type parameter T is, luckily, allowed to have an implicit index signature when we call X().


Let's test it out:

interface UserInfo {
    name: string;
    email: string;
}

Y<UserInfo>({ name: 'Jon', email: '[email protected]' }); // okay

Looks good. Now there's no error, because UserInfo is indeed assignable to Record<keyof UserInfo, string>, a.k.a. {name: string, email: string}.

Playground link to code

  • Related