Define a Record that can only have primitive values
A bit of a followup to this question on NoUnion types -- Is there a way to prevent union types in TypeScript?
Is it possible to construct a Record type such that the value of each property can be a primitive type but not a union of primitive types?
I.e., that these object would be valid:
{ a: string }
{ b: number }
{ c: boolean }
But these would not:
{ a: string | number }
{ b: boolean | string }
Solution 1:
There is no specific type in TypeScript which works the way you want. That is, you can't write anything like type RecordOfOnlyAllowedTypes = ...
and check that your candidate type is assignable to it by T extends RecordOfOnlyAllowedTypes
.
Instead, you can come up with a generic type type RecordOfOnlyAllowedTypes<T>
that acts as a constraint on a candidate type T
. If RecordOfOnlyAllowedTypes<T>
is a supertype of T
, then the constraint will succeed; otherwise it will fail. So you want to design RecordOfOnlyAllowedTypes<T>
to pass through a valid type unchanged, but transform an invalid one into something T
doesn't extend, such as a type where the offending properties are replaced with the never
type. Once you do this, anything that needs to verify that a type is valid would be of the form T extends RecordOfOnlyAllowedTypes<T>
, a technique known as "F-bounded quantification" that lets you represent types in terms of themselves.
(Note that this is also how you might use the NoUnion<T>
type from the answer to the other question also, by the way. You check T extends NoUnion<T>
instead of using some impossible-to-represent specific NoUnion
type.)
There are many possible implementations of RecordOfOnlyAllowedTypes
; here's one:
type AllowedTypes = [string, number, boolean]; // <-- put the types you want to allow in here
type _AT = Omit<AllowedTypes, keyof any[]>
type RecordOfOnlyAllowedTypes<T extends object> = {
[K in keyof T]: {
[I in keyof _AT]: (
T[K] extends _AT[I] ? (
_AT[I] extends T[K] ? (
T[K]
) : never
) : never
)
}[keyof _AT]
};
First, I wanted it to be easy to specify the allowed types. I didn't assume that they would be "primitive" or "not unions". Instead I have an AllowedTypes
type which is a tuple of the relevant types. I just made it string, number, boolean
. It's sort of an off-brand use of a tuple type; I don't care about the order; I mostly just want to keep the different types separated (so string | number | boolean
wouldn't work, because boolean
expands to true | false
) in an easy-to-look-at manner.
The next step is to turn AllowedTypes
into something easier to iterate, so Omit<AllowedTypes, keyof any[]>
uses the Omit<T, K>
utility type to strip anything "array-like" away from AllowedTypes
to produce _AT
, a type with just some keys for known element positions, like {0: string, 1: number, 2: boolean}
. Again, we don't care about the order, or the keys. {foo: string, bar: number, baz: boolean}
would be fine.
Armed with _AT
we can build the main functionality of RecordOfOnlyAllowedTypes<T>
. We make a mapped type over the properties of T
, checking each one against _AT
. So for a key K
, if the property type T[K]
is one of the elements of _AT
, we leave it alone. If it isn't, we map it to never
.
For each key I
in the keys of _AT
, we evaluate T[K] extends _AT[I] ? _AT[I] extends T[K] ? T[K] : never : never
. Essentially if _AT[I]
and T[K]
"mutually extend" each other, then we consider them equal (this isn't always true in TypeScript, but I'm not worried about any weird edge cases and you probably shouldn't either). Note that string
extends string | number
but the reverse isn't true. Then we form the union of those for all those keys of _AT
(by indexing into the inner mapped type with keyof _AT
). If T[K]
is equal to at least one of the property types of _AT
, then this mapped type will be T[K]
; otherwise it will be never
.
Okay, and finally, to help us test, we'll provide a check()
function with the constraint T extends RecordOfOnlyAllowedTypes<T>
and see what happens when we specify T
in different ways:
function check<T extends RecordOfOnlyAllowedTypes<T>>() { };
check<{ a: string }>(); // okay
check<{ b: number }>(); // okay
check<{ c: boolean }>(); // okay
check<{ a: string | number }>(); // error
check<{ b: boolean | string }>(); // error
Looks good! This is what you wanted. The first three are fine because the properties are each one of the AllowedTypes
elements, and the last two fail because they are not.
Playground link to code
Solution 2:
I would say that specific ask is impossible, because boolean
itself I believe is not a primitive, but a union itself of type true | false
.