2

I want a typescript type with a named property such that the name is provided dynamically, whilst also containing some other properties.

Something like:

type ItemWithNamespaceFlag<flagName>{
   name: string,
   color: "red"|"blue",
   [flagName]: boolean
}

let myRuntimeProperty: string = getStringFromSomewhere();
const ItemWithProperty: ItemWithNamespaceFlag<myRuntimeProperty>{
   name: "foo",
   color: "red",
   [myRuntimeProperty]: true
}

How can I achieve this?

3
  • 1
    You want unique types as per ms/TS#4895; these can be simulated with branding, but it doesn't really work the way you want as a key type. If flagName were a symbol instead of a string you could get similar behavior out of unique symbol like this; you might even be able to use this version and just lie to the compiler that myRuntimeProperty is a symbol instead of a string... but I don't know if that fits your use case. Does this fully address your question? If so I'll write up an answer; if not, what am I missing? Commented Jan 14, 2023 at 20:02
  • Wow, very detailed answer! That's exactly what I want, thank you! Still gotta lie to the compiler sometimes I guess... Commented Jan 14, 2023 at 23:17
  • 1
    Okay I’ll write up an answer post when I get a chance Commented Jan 14, 2023 at 23:18

2 Answers 2

2

(This answers the original question where FlagName was not dynamic).

Use intersection types: playground

type ItemWithNamespaceFlag<FlagName extends string> = {
   name: string,
   color: "red" | "blue",
} & Record<FlagName, boolean>
Sign up to request clarification or add additional context in comments.

4 Comments

Aha! but FlagName must be a literal... what if Flagname was dynamic?
What do you mean by dynamic? You can edit the question to better show the use case.
It can't be done. Typescript has no way of knowing at compile-time what getStringFromSomewhere will return at runtime. The most you can do is define the property like this: flag: { name: string, value: boolean }.
Ah dang, I suspected this would be the case. Thanks anyway!
1

You want the output of getStringFromSomewhere() to be a "unique" string type (let's call it MyRuntimeProperty) whose value you don't really know or care about; instead you want to "tag" it so that the compiler will only allow you to assign the same unique/tagged value as the key of ItemWithNamespaceFlag. This is similar to nominal typing, where MyRuntimeProperty would be a special subtype of string that isn't considered mutually compatible with other string types.

There are various feature requests for this sort of thing, such as unique types requested at microsoft/TypeScript#4895, but nothing is implemented. There are also various ways to simulate/emulate this sort of thing, such as branding (e.g., type MyRuntimeProperty = string & {__myRuntimeProperty}) , but unfortunately branded keys won't act the way you want.


So you'll need to work around it. Probably the simplest workaround is to pick a string literal type and pretend that this is the value you're going to have at runtime:

type MyRuntimeProperty =
  "__[[myRuntimeProperty]]__DO_NOT_USE_THIS_STRING_LITERAL"; // or whatever

Pick whatever string literal value you want, as long as it sufficiently discourages people from using the actual literal value. You're lying to the compiler about what the value is, but mostly a white lie.

Anyway, then things will just "work" for the most part:

declare function getStringFromSomewhere(): MyRuntimeProperty;
const myRuntimeProperty = getStringFromSomewhere();

interface ItemWithNamespaceFlag {
    name: string,
    color: "red" | "blue",
    [myRuntimeProperty]: boolean
};

const itemWithProperty: ItemWithNamespaceFlag = {
    name: "foo",
    color: "red",
    [myRuntimeProperty]: true
}

const v = itemWithProperty[myRuntimeProperty]
// const v: boolean

Technically nothing stops someone from writing

const k: MyRuntimeProperty = "__[[myRuntimeProperty]]__DO_NOT_USE_THIS_STRING_LITERAL"; 
itemWithProperty["__[[myRuntimeProperty]]__DO_NOT_USE_THIS_STRING_LITERAL"] 

in TypeScript, which would almost certainly fail at runtime. But hopefully anyone who does so will feel very ashamed of themselves.


A slightly more complicated workaround is to pretend to use a string enum which itself pretends to use a string literal:

const enum MyRuntimeProperty {
    "[[PretendKey]]" = "__[[myRuntimeProperty]]__DO_NOT_USE_THIS_STRING_LITERAL"
}    

This is treated a little more nominally than the plain string literal from before, in that now

const k: MyRuntimeProperty = "__[[myRuntimeProperty]]__DO_NOT_USE_THIS_STRING_LITERAL" // error

is an error, but object keys lose their enum-ness, so

itemWithProperty["__[[myRuntimeProperty]]__DO_NOT_USE_THIS_STRING_LITERAL"] 

is still allowed.


Another approach is to use unique symbol instead of a string type. JavaScript allows the use of symbols as unique keys, and TypeScript has the concept of unique symbol which acts as a nominal type, so no two separate declarations of unique symbol are compatible with each other. And TypeScript has explicit support for symbolic keys in a way that doesn't work for branded/tagged/enum strings. Of course your runtime string will definitely not be a symbol, so this is a bigger lie than the string literal... but it still works:

declare const MyRuntimeProperty: unique symbol
type MyRuntimeProperty = typeof MyRuntimeProperty;

declare function getStringFromSomewhere(): MyRuntimeProperty;
const myRuntimeProperty: MyRuntimeProperty = getStringFromSomewhere();
// need annotation ----> ^^^^^^^^^^^^^^^^^^^

interface ItemWithNamespaceFlag {
    name: string,
    color: "red" | "blue",
    [myRuntimeProperty]: boolean
};

const itemWithProperty: ItemWithNamespaceFlag = {
    name: "foo",
    color: "red",
    [myRuntimeProperty]: true
}

const v = itemWithProperty[myRuntimeProperty]
// const v: boolean

And now there's no way you could even try to use a string literal key to access the myRuntimeProperty property, because the compiler thinks it's a symbol, and it will only let you use the symbol that comes from myRuntimeProperty or the output of getStringFromSomewhere().


Playground link to code

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.