I making a game called risk using typescript and react hooks. The game is played on some kind of a map. So first of all I have design I MapEditor
. The state of Map Editor is like
export interface IMapEditorState {
mousePos: IPoint;
countries: {[k: string]: ICountry};
continents: { [k: string]: IContinent };
}
countries
and continents
are objects. The interface for country looks like
//The "name" property and above key will be same in `{[k: string]: ICountry};` will be same
export interface ICountry {
name: string;
border: IDot[];
neighbours: string[];
pleted: boolean;
}
Now I make a reducer function. For all the types of action I used two props name
and data
. name
will always be a string
and data will a type depending on name
type ActionTypes = {name: "removeCountry", data: string} | {name: "addCountry", data: ICountry};
const reducer = (state: IMapEditorState, action: ActionTypes) => {
...
}
Now see the first type in ActionTypes
which is {name: "removeCountry", data: string}
. In the dispatch method a I will use {name: "removeCountry"}
the piler will force to pass data
as string
but it couldn't be any string which I don't want. I want that I should only be able to pass string which is key of {[k: string]: ICountry}
in IMapEditorState
or the name
in ICountry
.
Is there any way that I could create a subtype of a string called CountryName
and use it
export interface IMapEditorState {
mousePos: IPoint;
countries: {[k: CountryName]: ICountry};
continents: { [k: string]: IContinent };
}
export interface ICountry {
name: CountryName;
border: IDot[];
neighbours: string[];
pleted: boolean;
}
type ActionTypes = {name: "removeCountry", data: CountryName} | {name: "addCountry", data: ICountry};
I will very thankful if you help me and kindly give your view on my data structure if got idea what is the game.
I making a game called risk using typescript and react hooks. The game is played on some kind of a map. So first of all I have design I MapEditor
. The state of Map Editor is like
export interface IMapEditorState {
mousePos: IPoint;
countries: {[k: string]: ICountry};
continents: { [k: string]: IContinent };
}
countries
and continents
are objects. The interface for country looks like
//The "name" property and above key will be same in `{[k: string]: ICountry};` will be same
export interface ICountry {
name: string;
border: IDot[];
neighbours: string[];
pleted: boolean;
}
Now I make a reducer function. For all the types of action I used two props name
and data
. name
will always be a string
and data will a type depending on name
type ActionTypes = {name: "removeCountry", data: string} | {name: "addCountry", data: ICountry};
const reducer = (state: IMapEditorState, action: ActionTypes) => {
...
}
Now see the first type in ActionTypes
which is {name: "removeCountry", data: string}
. In the dispatch method a I will use {name: "removeCountry"}
the piler will force to pass data
as string
but it couldn't be any string which I don't want. I want that I should only be able to pass string which is key of {[k: string]: ICountry}
in IMapEditorState
or the name
in ICountry
.
Is there any way that I could create a subtype of a string called CountryName
and use it
export interface IMapEditorState {
mousePos: IPoint;
countries: {[k: CountryName]: ICountry};
continents: { [k: string]: IContinent };
}
export interface ICountry {
name: CountryName;
border: IDot[];
neighbours: string[];
pleted: boolean;
}
type ActionTypes = {name: "removeCountry", data: CountryName} | {name: "addCountry", data: ICountry};
I will very thankful if you help me and kindly give your view on my data structure if got idea what is the game.
Share Improve this question asked Jan 25, 2020 at 7:38 Maheer AliMaheer Ali 36.7k7 gold badges49 silver badges82 bronze badges 4- @CertainPerformance I have created anything further. Just wanted to get a better way to further. – Maheer Ali Commented Jan 25, 2020 at 7:47
-
@CertainPerformance Consider a
function test(x : CountryName){...}
. Now here I want could only pass thename
property ofICountry
(For the time ignore the key of the object) – Maheer Ali Commented Jan 25, 2020 at 7:51 -
Given that the interface is
name: CountryName;
, typingfunction test(x : CountryName)
should already require that the parameter is of the permitted type, aCountryName
and nothing else, right? – CertainPerformance Commented Jan 25, 2020 at 7:59 -
@CertainPerformance I also want the keys of the
countries
to be of typeCountryName
. But if its not possible so please explain why not. – Maheer Ali Commented Jan 25, 2020 at 8:05
2 Answers
Reset to default 5If you want to be able to do these checks at pile time, you'll have to make a list of all possible country names:
type CountryName = 'cName1' | 'cName2' | 'cName3';
Or, if you can define an initial object of all possible countries, you can declare it as const
(so that TS doesn't generalize its strings), and then take its keys via keyof
:
const initialCountries = {
cName1: {
name: 'cName1',
pleted: false
// ...
},
cName2: {
name: 'cName2',
pleted: false
},
cName3: {
name: 'cName3',
pleted: false
},
} as const;
type CountryName = keyof typeof initialCountries;
Result for CountryName
is "cName1" | "cName2" | "cName3"
.
Then you can define IMapEditorState
using the above CountryName
:
export interface ICountry {
name: CountryName;
border: IDot[];
neighbours: string[];
pleted: boolean;
}
export interface IMapEditorState {
mousePos: IPoint;
countries: { [k: CountryName]: ICountry };
continents: { [k: string]: IContinent };
}
And then the following will pile:
const initalIMapEditorState: IMapEditorState = {
countries: initialCountries,
// ...
};
and then you can use CountryName
wherever else you need to:
type ActionTypes = {name: "removeCountry", data: CountryName} | {name: "addCountry", data: ICountry};
Typescript 4.1.5:
In my case I needed to mark a stringified date in this format "YYYY-MM-DD" as an ISODate rather than just a string. I used this approach, I think it's more effective than this answer if the number of valid strings is more than just a few:
interface ISODateDifferentiator extends String {
[key: string]: unknown;
}
export type ISODate = ISODateDifferentiator & string;
export const ValidateIsoDate = (date: string): date is ISODate => {
return date.match(/^\d{4}-\d{2}-\d{2}$/) !== null;
}
You get the desired Type 'string' is not assignable to type 'ISODate'.
when doing an unsafe assignment but can convert using the typeguard function. You also still get the string method auto-pletion.