Sammy
Sammy

Reputation: 3687

Dynamic String Literal Type from Variable in Typescript

I'm getting a token with claims from an authentication provider. It's a JSON object that looks like this:

{
  "email": "[email protected]",
  "urn:dev:claims/user-id": "123"
}

I'm trying to create a representative interface or type on my client to access this token keys correctly. The problem is the "dev" part of "urn:dev:claims/user-id" above is dynamic, it comes from environment variable as such:

const claimKey = urn:${process.env.REACT_APP_ENV}:claims/user-id

When I try the following:

interface MyInterface {
  email: string;
  [claimKey]: string;
}

It does not work.

Here's a full reproducible example:

const env = {
  REACT_APP_ENV: "dynamic-value"
}

const token = {
  "email": "[email protected]",
  "urn:dev:claims/user-id": "123"
}

const claimKey = `urn:${env.REACT_APP_ENV}:claims/user-id`

interface MyInterface {
  email: string;
  [claimKey]: string;
}

Upvotes: 2

Views: 6070

Answers (1)

jcalz
jcalz

Reputation: 328362

TypeScript does not currently have the capacity to do exactly what you're asking for:

  • You want the compiler to treat process.env.REACT_APP_ENV as some "unknown but unique" string literal, much the way that unique symbol works for symbol-typed values. There was an experimental pull request at microsoft/TypeScript#33038 which would have allowed things like unique string, but it never made it into the language.

  • Moreover, you need to be able to concatenate that unique string to other string literals and have some sort of unique output; maybe this would also require supporting unique string inside "pattern" template literal types as implemented in microsoft/TypeScript#40598, and it isn't obvious that this would work.

  • And even if that were all taken care of, you currently can't use pattern template literal types as object keys; see microsoft/TypeScript#42192. An object type like Record<`foo${string}`, number> is unfortunately treated very much like {}; it will not complain if you assign a type like {fooOops: "This is not a number"} to it. ( Well, at least this part is fixed for TS4.4; pattern template literals can be used in index signatures as per microsoft/TypeScript#44512 )

All of that put together means this is just not within the realm of possibility for TypeScript as of TS 4.4.


Instead you would need some sort of workaround. I was toying around with using a string enum to simulate an opaque/nominal subtype of string that works with keys, but it doesn't really do anything more useful than the workaround I settled on: a placeholder string literal type like "###${process.env.REACT_APP_ENV}###" that we pretend is the known actual type of process.env.REACT_APP_ENV. As long as we only refer to the type as process.env.REACT_APP_ENV and not as the pretend string literal, everything will work out. We might even want the pretend value to be something like "!!!PLACEHOLDER_DO_NOT_USE_THIS!!!" or whatever you need to convince people not to use the literal type.

It would look like this:

declare const process: {
  env: {// the following type is a fiction but is the best I can do 
    REACT_APP_ENV: "###${process.env.REACT_APP_ENV}###"
  }
}

And then your claimKey would be a const-asserted template string so that the compiler can concatenate it and maintain the string-literal-ness of it:

const claimKey = `urn:${process.env.REACT_APP_ENV}:claims/user-id` as const

And everything works as desired, mostly:

interface MyInterface {
  email: string;
  [claimKey]: string;
}

const myInterface: MyInterface = {
  email: "[email protected]",
  [claimKey]: "123"
}

Hooray! Still, it's just a workaround. That placeholder value will likely show up as IntelliSense hints, unfortunately:

const booIntelliSense: MyInterface = {
  email: "",
  "urn:###${process.env.REACT_APP_ENV}###:claims/user-id": "oops" // <-- hinted by IntelliSense!
}

so it's really not perfect. Oh well.

Playground link to code

Upvotes: 6

Related Questions