There are many similar question regarding how to assert that a particular key is of a particular interface or type, like this question. My question is different because the keys in question are made from two parts rather than simply consuming a key from something like Object.keys
.
This seems to do with mapped types but I don't even know if this is a problem TypeScript can solve. Currently, there are many interfaces I have that are full of property names that are predictable combinations of a name prefix, like team
, and a number part. The numbers are always in some range like 1-16
or 1-18
. I already added these interfaces without mapped types, so they are very large interfaces and I'm hoping to use mapped types going forward.
I'm converting files gradually from JavaScript to TypeScript. Most of the newly converted TypeScript code that is still improperly typed as the any
type. It involves iterations of the previous ranges like follows:
const name = record[`team${num}`]; // num is from 1-16
I know that record['team${num}']
is not any
but in fact string
in type. If I were to simply write out the dozens of property names using dot notation a la record.team1;
then there is no error and type checking works fine. Neither the simple assertion, as string
, nor as unknown as string
, nor as (typeof record[keyof typeof record])
is removing the error messages from the problematic code. I know how to suppress type errors with type assertions now, but notice the code would lead to an off by one error and there is now no error when it will in fact be a runtime error.
Is there an idiomatic way to check the type using TypeScript? I don't want off-by-one errors, etcetera. Some of my code can be rewritten to consume the existing properties, while other code creates new properties from those existing properties that are indexed with a combination of name and number.
You could use so known Template Literal Types. As long as the teamnames and count are known. A possible solution could look like this:
type TeamNumber = 1 | 2
type TeamName = 'TeamA' | 'TeamB'
type TeamData = { trainer?: string }
// here is where the magic happens:
// we compute all keys based on other union types inside a template string
type TeamRecord = {
[K in `${TeamName}${TeamNumber}`]: TeamData;
};
// you get autocompletion for all properties
const teamRecord: TeamRecord = {
TeamA1: {},
TeamA2: {},
TeamB1: {},
TeamB2: {}
}
Feel free to play around with this TypescriptPlayground.
EDIT:
You can also create the TeamNumber type from an array if you need to itereate through the values of the TeamNumber. Thus there is no need for a const max
variable to prevent falsy values inside the iteration.
const teamNumbers = [1, 2, 3, 4] as const;
type TeamNumber = typeof teamNumbers[number];
type Team = 'team';
type VerboseTeamRecord = Teams & {
id: number;
event: string;
gender: 'M' | 'F' | 'B';
};
type Teams = {
[K in `${Team}${TeamNumber}`]: string;
};
let verboseTeamRecord: Teams = { team1: 'A', team2: 'B', team3: 'C', team4: 'D' };
teamNumbers.forEach(teamNumber => {
const k = `team${teamNumber}` as keyof Teams;
console.log(verboseTeamRecord[k]);
});
Here is the Typescriptplayground for the edit.