brightpants
brightpants

Reputation: 525

Can I map a string literal to a type of types?

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

Answers (2)

brightpants
brightpants

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

jcalz
jcalz

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; }'.

Playground link to code

Upvotes: 9

Related Questions