最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

typescript - Creating wildcard variables in types (or interfaces) - Stack Overflow

programmeradmin1浏览0评论

I am working on a testing suite for my workplace that uses .env for user-related variables. They have a special userConfig script to get the variables so I am not permitted to use process.env syntax directly in my code. I already need to make edits to their script to allow for variables specific to my tests, but there is one edit that I don't know how to make that will save a lot of effort in the long run.

The users that I need to add to my .env files have "account types," which will be expanded on in future tests that I will need to write. For example, a user in .env might look like this:

USER1_USERNAME = user1
USER1_PASSWORD = password1
USER1_ACCOUNT_FIRSTTYPE = user1account1
USER1_ACCOUNT_SECONDTYPE = user1account2
USER1_ACCOUNT_THIRDTYPE = user1account3

There will be many more account types than this in later updates to the tests, some of which I can't predict, and I'd rather not have to edit the userConfig code every time I add a new one. What I hope to do is add a "wildcard" account type to the userConfig, making it easier to create variables for any account type that I happen to use in the future.

Here is the current userConfig code that gets values from .env:

export type UserInfo = {
    username?: string,
    password?: string,
}
function getUserInfo(): Map<string, UserInfo> {
    const userCredentialsMap = new Map<string, UserInfo>();

    for (const [key, value] of Object.entries(process.env)) {
        for (const suffix of ['_USERNAME', '_PASSWORD'] {
            if (key.endsWith(suffix)) {
                const prefix = key.slice(0, -suffix.length);
                if (!userCredentialsMap.has(prefix)) {
                    userCredentialsMap.set(prefix, {});
                }
                switch (suffix) {
                case '_USERNAME':
                    userCredentials.username = value;
                    break;
                case '_PASSWORD':
                    userCredentials.password = value;
                    break;
                }
            }
        }
    }

    return userCredentialsMap;
}

Is there a way to add a flexible, wildcard-esque variable to this code, so that it could read any .env value with "_ACCOUNT_" at the beginning? It would have the effect of something like this:

export type UserInfo = {
    username?: string,
    password?: string,
    account.*?: string,
}
case '_ACCOUNT_*':
    userCredentials.account.* = value;
    break

Result: USER1_ACCOUNT_FIRSTTYPE would be saved as userCredentials.account.FIRSTTYPE, USER1_ACCOUNT_OTHERTYPE would be saved as userCredentials.account.OTHERTYPE, etc.

(I know the above code wouldn't work, but it's the simplest way I could come up with to show the effect I'm looking for.)

I am working on a testing suite for my workplace that uses .env for user-related variables. They have a special userConfig script to get the variables so I am not permitted to use process.env syntax directly in my code. I already need to make edits to their script to allow for variables specific to my tests, but there is one edit that I don't know how to make that will save a lot of effort in the long run.

The users that I need to add to my .env files have "account types," which will be expanded on in future tests that I will need to write. For example, a user in .env might look like this:

USER1_USERNAME = user1
USER1_PASSWORD = password1
USER1_ACCOUNT_FIRSTTYPE = user1account1
USER1_ACCOUNT_SECONDTYPE = user1account2
USER1_ACCOUNT_THIRDTYPE = user1account3

There will be many more account types than this in later updates to the tests, some of which I can't predict, and I'd rather not have to edit the userConfig code every time I add a new one. What I hope to do is add a "wildcard" account type to the userConfig, making it easier to create variables for any account type that I happen to use in the future.

Here is the current userConfig code that gets values from .env:

export type UserInfo = {
    username?: string,
    password?: string,
}
function getUserInfo(): Map<string, UserInfo> {
    const userCredentialsMap = new Map<string, UserInfo>();

    for (const [key, value] of Object.entries(process.env)) {
        for (const suffix of ['_USERNAME', '_PASSWORD'] {
            if (key.endsWith(suffix)) {
                const prefix = key.slice(0, -suffix.length);
                if (!userCredentialsMap.has(prefix)) {
                    userCredentialsMap.set(prefix, {});
                }
                switch (suffix) {
                case '_USERNAME':
                    userCredentials.username = value;
                    break;
                case '_PASSWORD':
                    userCredentials.password = value;
                    break;
                }
            }
        }
    }

    return userCredentialsMap;
}

Is there a way to add a flexible, wildcard-esque variable to this code, so that it could read any .env value with "_ACCOUNT_" at the beginning? It would have the effect of something like this:

export type UserInfo = {
    username?: string,
    password?: string,
    account.*?: string,
}
case '_ACCOUNT_*':
    userCredentials.account.* = value;
    break

Result: USER1_ACCOUNT_FIRSTTYPE would be saved as userCredentials.account.FIRSTTYPE, USER1_ACCOUNT_OTHERTYPE would be saved as userCredentials.account.OTHERTYPE, etc.

(I know the above code wouldn't work, but it's the simplest way I could come up with to show the effect I'm looking for.)

Share Improve this question edited Jan 31 at 19:59 jcalz 331k29 gold badges441 silver badges442 bronze badges asked Jan 30 at 17:04 MaltMalt 213 bronze badges 2
  • 1 Is the approach at this playground link what you're looking for? If so I could write an answer or find a duplicate; if not, what am I missing? – jcalz Commented Jan 30 at 17:20
  • @jcalz That looks like what I'm aiming for, BUT would you mind using more descriptive variable names instead of just letters? That's one requirement on my team and it would also help me understand the new code that I'll have to add. – Malt Commented Jan 30 at 17:33
Add a comment  | 

2 Answers 2

Reset to default 0

You can do this for your type:

export type UserInfo = {
    username?: string,
    password?: string,
} & {
    [K in `acount.${string}`]?: string
};

To break it down:

  • `acount.${string}`
    
    This is a template literal type, it essentially says 'x is of type `acount.${string}` if there exists a y of type string such that x === `acount.${y}`'.
  • {
        [K in `acount.${string}`]?: string
    }
    
    This is a mapped type, it says 'an object where for a key K of type `acount.${string}`, the (optional) value is of type string.
  • export type UserInfo = {
        username?: string,
        password?: string,
    } & {
        [K in `acount.${string}`]?: string
    };
    
    This is an intersection type, it combines two object types into one object type that requires the keys from both objects to be present.

As for your switch case, I don't think there's a way to switch on the start of a string, but you can check if a string starts with a certain prefix by using string.startsWith(prefix), and you can then get the string without the prefix by doing string.slice(prefix.length).

The type you're looking for is straightforward; the account property of UserInfo should have a string index signature:

type UserInfo = {
    username?: string,
    password?: string,
    account?: { [k: string]: string } 
}

I've made it optional, like username and password. That means if a UserInfo has an account property, it will be an object whose property keys are arbitrary strings (that's the [k: string]) and whose values at those keys are also strings (that's the : string).


As for the implementation of getUserInfo(), the particulars are up to you and how you want edge cases to be treated. The following implementation works for your particular example. Please make sure you test any implementation thoroughly against edge cases and adjust accordingly.

function getUserInfo(): Map<string, UserInfo> {
    const userInfoMap = new Map<string, UserInfo>();
    for (const [key, value] of Object.entries(process.env)) {
        const [userKey, property, subProperty] = key.split("_", 3);
        if (!userInfoMap.has(userKey)) userInfoMap.set(userKey, {});
        const userInfo = userInfoMap.get(userKey)!
        switch (property) {
            case 'USERNAME': userInfo.username = value;
                break;
            case 'PASSWORD': userInfo.password = value;
                break;
            case 'ACCOUNT':
                if (!userInfo.account) userInfo.account = {};
                userInfo.account[subProperty] = value;
                break;
        }
    }
    return userInfoMap;
}

I've elected to split() your keys at underscore characters, storing the first three chunks into userKey, property, and subProperty, respectively. So "USER1_USERNAME" results in a userKey of "USER1" and a property of "USERNAME", and subProperty is undefined. And "USER1_ACCOUNT_FIRSTTYPE" results in a userKey of "USER1", a property of "ACCOUNT" and a subProperty of "FIRSTTYPE". Again, this works for your example code, but one can easily imagine edge cases where this does unexpected things, so beware.

Anyway, we use userKey as a key into the userInfoMap, adding an empty object if necessary, and then we switch on property. For "USERNAME" and "PASSWORD" we just set the appropriate property on the relevant map entry. For "ACCOUNT", we add an empty object at the account property if necessary (because it's optional), and then use subProperty as the key to that account property object.


You can verify that it behaves as expected for your example:

const userInfoMap = getUserInfo();
console.log(userInfoMap)
/* Map (1) {"USER1" => {
  "username": "user1",
  "password": "password1",
  "account": {
    "FIRSTTYPE": "user1account1",
    "SECONDTYPE": "user1account2",
    "THIRDTYPE": "user1account3"
  }
}}  */

Playground link to code

发布评论

评论列表(0)

  1. 暂无评论