Home > Software engineering >  Typescript type with property of name given as parameter
Typescript type with property of name given as parameter

Time:01-16

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?

CodePudding user response:

(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>

CodePudding user response:

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

  • Related