Here's a simple example: an API returns a list of possible options with the following structure:
{
"familystatus": [
{
"Id": 1,
"Name": "single"
},
{
"Id": 2,
"Name": "married"
}
],
"gender": [
{
"Id": 1,
"Name": "male"
},
{
"Id": 2,
"Name": "female"
}
]
}
Which translates to Typescript (as example) into something like:
export interface Table {
familyStatus: LookupSelection[];
gender: LookupSelection[];
}
export interface LookupSelection {
id: number;
name: string;
}
When I want to display those options in a list, the separation of concern principle would force me to define a different type:
export interface SelectboxOption {
id: any;
name: string;
}
So it happens to be that both server response and the client code overlap (so I can directly bind the server result into my client model), but it could as well be different (i.e. SelectboxOption
had label
and code
instead of id
and name
.)
- Should I forcibly design my client-oriented model to overlap the server model? (Less code and adds "transparency" - but unclear and breaks common principles)
- Should I use in-class converters (make
SelectboxOption
a class with a constructor, for example)? (Fail-safe but not as clean as other options) - Should I make a converter class? (Most explicit but feels redundant)
Either way I go, it feels like breaking a law.
Thanks!
2 Answers 2
From a pragmatic point of view:
What is it that you want to accomplish? If you group data into types, you want to achieve that it acts according to a certain contract:
Looking at your example, the data of familystatus
and gender
, you have two fields id
and name
and their respective types int
and string
.
So from what was said above, it would totally make sense to identify a common type, e.g. NumericOption
.
Its name leads the reader to the usecase : it is an option type where the key is a number
In a possible application there could be a StringOption
which is different in name and purpose. Given one or the other you get what you expected. But you could not take one for the other.
Looking at your code
export interface SelectboxOption {
id: any;
name: string;
}
any
is the signal word here. By having this, you make a contract:
I am a Key-Value
structure, where the only thing which is type-enforced is, having a name being a string
.
So, if you need to present your data in a list abstracting from the type of its key, a general Type would be ok.
So it happens to be that both server response and the client code overlap (so I can directly bind the server result into my client model), but it could as well be different (i.e. SelectboxOption had label and code instead of id and name.)Problems arise, if you need the more specific type - say to fill a form.
What do you mean here? Is it unclear, what exactly comes from the server?
Then you would have to make a more abstract type like an array
of key-value
-pairs or the like.
But chances are good that when there are type definitions on the server side, even implicite ones where the type could be inferred, that you could replicate them on the client side with Typescript.
tl;dr
Types help to group things under a contract.
Every object of the same kind has the same structure / contract.
Using any
softens the contract.
Is it a good/bad practice to create overlapping types?
Using types gives some kind of security regarding your data. And the use of a typed language on the server-side as well as on the client-side would give you the same kind of security. So having corresponding types would be consequential.
I hope this helps
You should type the response from the server and your client code separately. TypeScript is very flexible with its structural typing, so I would keep the names of the fields in your client code the same if possible so you don't have to do any conversions. For example:
export interface Table {
familyStatus: FamilyStatusRecord[];
gender: GenderRecord[];
}
export interface FamilyStatusRecord {
id: number,
name: "single" | "married"
}
export interface GenderRecord{
id: number,
name: "male" | "female" | "other"
}
export interface SelectboxOption {
id: number;
name: string;
}
const tbl: Table = {
"familyStatus": [
{"id": 1,"name": "single"},
{"id": 2,"name": "married"}
],
"gender": [
{"id": 1,"name": "male"},
{"id": 2,"name": "female"}
]
};
function showOptions(options: SelectboxOption[]) {
//todo: show options
}
showOptions(tbl.familyStatus);
showOptions(tbl.gender);
Notice how the showOptions
function will accept an FamilyStatusRecord[]
or a GenderRecord[]
even though it declared options to be a SelectBoxOption[]
. Now if you do need to change the SelectboxOption
interface. You will get a compile error, and you will know exactly where to fix it.
Explore related questions
See similar questions with these tags.
Should I forcibly design my client-oriented model to overlap the server model?
curiously, it's the other way around. You model the server response the way that best works for the client-side. It means that the type conversion happens on the server side. The controller transforms the domain|business model into a model that clients will appreciate most. As @ThomasJunk answered, it's not best practice, it's just consequential