How to create a lookup where the type of each value depends on the key?

Viewed 60

I'm trying to create a lookup where each key determines the type of the corresponding value.

Consider this code:

type Language =  'en' | 'de';

// Language-specific parser
type Parser<TLanguage extends Language> = (text: string, language: TLanguage) => any;

// Lookup that contains a typed parser for each language
const parsers: { [language in Language]: Parser<language> } = {
  'de': () => null, // Dummy values
  'en': () => null,
};

The idea is that for each Language, parsers contains a language-specific parser strongly typed for that particular language.

This assignment works as expected:

const englishParser: Parser<'en'> = parsers.en;

But this assignment doesn't work, and I can't figure out why. My goal is to write type-safe code without type assertions.

function test<TLanguage extends Language>(language: TLanguage) {
  // Error:
  // Type '{ en: Parser<"en">; de: Parser<"de">; }[TLanguage]' is not assignable
  // to type 'Parser<TLanguage>'.
  const parser: Parser<TLanguage> = parsers[language]; //
}

Here's a TypeScript Playground link that demonstrates the problem.

2 Answers

You need a type cast in there :

function test<TLanguage extends Language>(language: TLanguage) {
  const parser: Parser<TLanguage> = parsers[language] as Parser<TLanguage>; 
}

Playground Link

You don't need the generics:

function test(language: Language) {
    const parser = parsers[language];
}

Explanation as to why what you try to do cannot work:

The type of parsers does not retain the information with which you constructed it. In the declaration { [language in Language]: Parser<language> } there is a clear dependence between the name of the property and the type of the property.

However, this type gets resolved to:

const parsers: {
    en: Parser<"en">;
    de: Parser<"de">;
}

Which can be seen as

const parsers: {
    en: BlackBoxA;
    de: BlackBoxB;
}

So if you access a property with the key 'en' | 'de' you will simply get BlackBoxA | BlackBoxB, or here Parser<"en"> | Parser<"de"> which cannot necessarily be assigned to your more specific type Parser<TLanguage>, which is just one of the two and incompatible with the other one.

Related