Reputation: 810
I would like to declare a value that extends specific type and keeps its narrow type at the same time.
Is there a way to achieve this without calling a function?
const stringRecord : <T extends Record<string, string>>(x: T)=>T= (x) => x;
//Define value that extends Record<string, string> without calling a function
const abc = stringRecord({
a: "a",
b: "b",
c: "c"
});
//Type should remain {a: string, b: string, c: string}
type ABC = typeof abc;
Upvotes: 1
Views: 53
Reputation: 328342
No, there is currently (as of TypeScript 4.4) no type operator which checks that a value is assignable to a given (non-union) type without also widening it to that type. There is a longstanding open feature request for such an operator at microsoft/TypeScript#7481. If you want to see this happen you might go to that issue and give it a 👍 or describe your use case and why the existing workarounds aren't sufficient; but I don't think it's likely that a single additional voice there will have much effect.
For now there are, unfortunately, only workarounds.
The remainder of this answer will discuss some of them briefly; although since they are not acceptable to you, you may ignore it. Consider the discussion academic, or possibly helpful to future readers.
The workaround from your question, using a helper function, is a good one in my opinion. It has some minimal runtime impact, but it's not too cumbersome to use.
If you don't want any runtime impact at all, you could write some type functions to do something similar but purely in the type system:
const abc = {
a: "a",
b: "b",
c: "c"
};
type ABC = typeof abc;
type Extends<T extends U, U> = void;
type TestABC = Extends<ABC, Record<string, string>>; // okay
The Extends<T, U>
type function doesn't evaluate to anything useful (it's just void
), but it will throw a compiler warning if T
does not extend U
. Observe:
const abc = {
a: "a",
b: 123, // <-- not a string
c: "c"
};
type ABC = typeof abc;
type Extends<T extends U, U> = void;
type TestABC = Extends<ABC, Record<string, string>>; // error!
// ------------------> ~~~
// Property 'b' is incompatible
And all the extra stuff gets erased at runtime.
Finally, the simplest workaround is just to ignore the constraint entirely and rely on something elsewhere to throw a compiler error. That is, if you write just
const abc = {
a: "a",
b: "b",
c: "c"
};
and move on, presumably somewhere else in your code base you will use abc
in a place that expects a Record<string, string>
. Perhaps by passing it to some function you actually want to use:
declare function doSomethingLater(x: Record<string, string>): void;
If it works with no compiler warning, then great:
doSomethingLater(abc); // okay
If not, the warning will tell you what was wrong with abc
:
doSomethingLater(abc); // error!
/* Argument of type '{ a: string; b: number; c: string; }' is not
assignable to parameter of type 'Record<string, string>' */
If no code in your code base complains when abc
is not assignable to Record<string, string>
, then you might want to step back and consider why that is. Maybe you don't actually need such a constraint because the code doesn't care. But if the absence of a warning doesn't imply that everything's fine, then you might need to fall back to one of the previous workarounds.
Upvotes: 1