Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Narrow string literal type dynamically based on code

Sorry if the title is not accurate, It’s kind of a weird question.

I have the following code:

enum LogType {
  file = "file",
  http = "http",
  database = "database"
}

interface LoggerDetails {
  // ... bunch of properties
  provider: "datadog" | "log4j" | "newrelic"
}

interface Logger {
  type: LogType;
  provider: LoggerDetails["provider"];
}


interface NetworkLogger extends Logger {
  type: LogType.http,
  provider: "datadog"
}

interface FileLogger extends Logger {
  type: LogType.file,
  provider: "log4j"
}

type LoggerKey = `${LogType}.${Logger["provider"]}`;

// type LegalLoggerKeys = Extract<LoggerKey, "http.datadog" | "file.log4j">;// <-- This works, but hard coded. Also, it requires us to write it down for every new logger interface
type LegalLoggerKeys = unknown; // ?? 

// should only accept 'http.datadog' and 'file.log4j' as keys using the interfaces that extends Logger 
const record: Record<LegalLoggerKeys, (message: string) => void> = {

}

My goal is to create LegalLoggerKeys using the interfaces FileLogger, NetworkLogger, or any other implementation. as you can see my commented code, I was able to do it with hard coded string literals.

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

Is it possible to create the same thing using the interfaces?
Playground Link

>Solution :

The compiler won’t be able to automatically discover all interfaces declared to extend Logger, so you’ll need to maintain such a list yourself manually. One way to do that is with a union:

type KnownLoggers = NetworkLogger | FileLogger // <-- maintain this manually

Once you have such a union, you can manipulate it to generate the union of acceptable keys. If you have a single logger type L that extends Logger, you can compute the allowed key via the template literal type:

type LoggerKey<L extends Logger> = `${L['type']}.${L['provider']}`;

type NetworkLoggerKey = LoggerKey<NetworkLogger>;
// type NetworkLoggerKey = "http.datadog"

But if you have a union of logger types, this won’t behave the way you want, since each piece of that concatenated string will be split into unions separately:

type KnownLoggerKeys = LoggerKey<KnownLoggers>
// type KnownLoggerKeys = "file.datadog" | "file.log4j" | "http.datadog" | "http.log4j" 👎

Oops.


So we will need to tweak the definition of LoggerKey so that it distributes its operation across unions in its input. We can do this by expressing it as a distributive conditional type:

type LoggerKey<L extends Logger> =
  L extends Logger ? `${L['type']}.${L['provider']}` : never;

Note that the check L extends Logger ? isn’t really useful as a check; the compiler already knows that L is constrained to Logger. But it serves as a directive to split L up into its union members before evaluation and then to re-unite into a new union afterward.

And so now we have:

type KnownLoggerKeys = LoggerKey<KnownLoggers>
// type KnownLoggerKeys = "http.datadog" | "file.log4j" 👍

as desired.

Playground link to code

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading