Reputation: 525
I have a string literal type, such as
type ConfigurationTypes = 'test' | 'mock'
and some types
type MockType = { id: string }
type TestType = { code: string }
And I wanted to create a type that "maps" the string literal to this types, so that if ConfigurationTypes
changes, my type MappedConfigurationTypes
would also be required to change accordingly. Is it even possible?
type MappedConfigurationTypes: {[key in ConfigurationTypes]: any} = {
test: TestType
mock: MockType
}
Upvotes: 4
Views: 787
Reputation: 525
I did some fidgeting to the code provided by jcalz and I arrived at a slightly better solution for my problem:
type MapFromLiteral<U extends string, T extends { [key in U]: any }> = T;
Which can be used as
type MappedConfigurationTypes = MapFromLiteral<ConfigurationTypes, {
test: TestType,
mock: MockType
}>
Upvotes: 2
Reputation: 327754
In some sense you're looking for a type-level satisfies
operator. If you write e satisfies T
where e
is some expression and T
is some type, the compiler will make sure that e
is assignable to T
without widening to T
, so e
keeps its original type but you'll get an error if is incompatible with T
. You want to do the same thing but replace the expression with another type. Something like
// this is invalid TS, don't do this:
type MappedConfigurationTypes = {
test: testType;
mock: MockType
} Satisfies {[K in ConfigurationTypes]: any}
but there is no such Satisfies
type operator. Too bad.
Luckily we can essentially build one ourselves: instead of T Satisfies U
, we could write Satisfies<U, T>
(I'm making "Satisfies U
" the syntactic unit of note, so that's why I want Satisfies<U, T>
and not Satisfies<T, U>
. But you can define it however you want).
Here's the definition:
type Satisfies<U, T extends U> = T;
You can see how Satisfies<U, T>
will always evaluate to just T
, but since T
is constrained to U
, the compiler will complain if T
is not compatible with U
.
Let's try it:
type ConfigurationTypes = 'test' | 'mock';
type MockType = { id: string }
type TestType = { code: string }
type MappedConfigurationTypes = Satisfies<{ [K in ConfigurationTypes]: any }, {
test: TestType
mock: MockType
}>
Looks good. If you hover over MappedConfigurationTypes
you see it is equivalent to
/* type MappedConfigurationTypes = {
test: TestType;
mock: MockType;
} */
On the other hand if you add another member to the ConfigurationTypes
union, you'll see the desired error:
type ConfigurationTypes = 'test' | 'mock' | 'oops'
type MappedConfigurationTypes = Satisfies<{ [K in ConfigurationTypes]: any }, {
test: TestType
mock: MockType,
}> // error!
// Property 'oops' is missing in type '{ test: TestType; mock: MockType; }' but required
// in type '{ test: any; mock: any; oops: any; }'.
Upvotes: 9