Chapter 6: Data Models#
Welcome back! In Chapter 5: Main App Logic, we explored how the MainView
component acts as the central conductor for our application, managing state and coordinating actions between different parts like the photo upload screen, the map view, and the info panel.
The conductor needs music to conduct! That music is the data flowing through the application β the selected photo file, the extracted location data, the calculated building measurements, the list of gallery images, and so on. But for the different parts of the orchestra (our components and functions) to understand this data, they need a common language or format.
What's the Problem?#
Imagine you're working with a photo file. You extract some information from its EXIF tags. This information might include the camera make, the date and time it was taken, and crucially, the GPS coordinates (latitude, longitude, altitude).
If you just pass this information around as a generic JavaScript object, like { Make: 'NIKON', GPS: { Latitude: '...', Longitude: '...', Altitude: '...' } }
, how does the part of the code that uses the GPS data know exactly what properties to look for? Is it GPS.Latitude
or GPS.lat
? Is the altitude a number or a string? What if the photo had no GPS data at all?
Without a clear definition of how data objects should be structured, developers might guess the property names, leading to errors (like trying to read someObject.latitude
when the actual property is lat
) or unexpected behavior when data is missing or in the wrong format. This makes the code harder to write, understand, and maintain, especially in larger projects.
What are Data Models?#
This is where Data Models come in. In our project, data models are formal definitions of what a specific piece of data should look like. They act like blueprints or contracts for the shape of our data objects.
In TypeScript, the language our project is written in, we define these data models using interfaces. An interface specifies:
- The names of the properties an object should have.
- The types of values those properties should hold (e.g.,
string
,number
,boolean
,File
,Record<string, any>
,string[]
).
Think of an interface like a recipe card for a specific dish:
Recipe: Chocolate Chip Cookies
--------------------------------
Ingredients:
- Flour: number (cups)
- Sugar: number (cups)
- Butter: number (cups)
- Chocolate Chips: number (cups)
- Eggs: number
- Vanilla Extract: number (teaspoons)
- Baking Soda: number (teaspoons)
- Salt: number (teaspoons)
Instructions: string (text describing steps)
BakingTime: number (minutes)
Temperature: number (degrees Fahrenheit)
This "recipe interface" tells you exactly what ingredients are needed, their types (how to measure them), and what other information (instructions, time, temp) is included, along with their types. If you try to make "Chocolate Chip Cookies" using an object { flour: 'two cups', Sugar: 2 }
, TypeScript would look at the interface and say, "Hey! 'flour' should be a number
, not a string'
, and 'Sugar' should be lowercase 'sugar'!".
Using interfaces provides several key benefits:
- Clarity: It makes it immediately clear to anyone reading the code what data is expected.
- Safety: TypeScript uses interfaces to check your code before you run it. If you try to access a property that doesn't exist on an object according to its interface, or if you try to assign a value of the wrong type, TypeScript will show an error. This prevents many common bugs.
- Consistency: It ensures that data objects used throughout the application have a predictable and consistent structure.
- Documentation: Interfaces serve as living documentation for the data structures used in the project.
Our data models are primarily defined in the src/types/
folder.
Project Data Models: Blueprints for Information#
Let's look at some of the specific data models (interfaces) used in our project to represent different types of information:
Metrics Data (Metrics
and ComputedMetric
)#
In Chapter 4: Building Information Display, we saw the BuildingAttributes
component display calculated metrics like land area, building area, and height. The data for these metrics is structured using the Metrics
interface defined in src/types/metrics.ts
:
// src/types/metrics.ts
export interface Metrics {
landArea: number;
buildingArea: number;
volume: number;
buildingHeight: number;
}
This interface tells us that any object claiming to be Metrics
must have the properties landArea
, buildingArea
, volume
, and buildingHeight
, and each of these properties must be a number
.
We also saw the MetricDisplay
component (Chapter 4) which displays a single metric. Its data is structured using the ComputedMetric
interface defined in src/types/metric.ts
:
// src/types/metric.ts
export interface ComputedMetric {
label: string; // e.g., "Land Area"
unit: string; // e.g., "m2"
value: number; // e.g., 150.75
}
This interface specifies that a single metric object needs a label
(a string
), a unit
(a string
), and a value
(a number
). This ensures that when BuildingAttributes
passes data to MetricDisplay
, it provides exactly the structure MetricDisplay
expects.
User Input Structure (UserInputs
)#
Sometimes, user input comes from forms or sliders, not just file uploads. The structure for some of this input (though not heavily used in the provided code snippets) might be defined like this in src/types/user-inputs.ts
:
// src/types/user-inputs.ts
export interface UserInputs {
// Example: data from input fields for building calculation overrides
[lotCoverage: string]: number; // This uses an index signature, less common for beginners.
// It essentially means "I expect properties whose names are strings,
// and their values are numbers". Let's simplify the explanation.
// Simplified version (imagine specific inputs):
// lotCoveragePercentage: number;
// numberOfFloors: number;
// averageFloorHeight: number;
}
Self-correction: The actual UserInputs
interface uses an index signature [lotCoverage: string]: number;
, which might be too complex for a very beginner tutorial. The provided snippet also includes floorNumber
and floorHeight
. Let's stick to explaining the simpler properties and mention the index signature gently or simplify the code example.
Let's use the simpler properties shown in the provided UserInputs
snippet:
// src/types/user-inputs.ts
export interface UserInputs {
floorNumber: number;
floorHeight: number;
// [lotCoverage: string]: number; // Keeping this in comment to reflect original, but focusing on the others.
}
This interface tells us that expected user input data might include floorNumber
and floorHeight
, both as number
s. If a function expects a UserInputs
object, TypeScript will check if the object you provide has these number properties.
File Metadata (NginxFile
)#
In components like BuildingAttributes
(Chapter 4) and potentially when fetching data (Chapter 7: API Data Fetching), we might get a list of files available on a server. The structure of the objects representing these files is defined by the NginxFile
interface in src/types/nginx.ts
:
// src/types/nginx.ts
export interface NginxFile {
name: string; // e.g., "building_scan_1.laz"
type: string; // e.g., "file" or "directory"
mtime: string; // e.g., "2023-10-27T10:30:00Z" (Modification time)
size: string; // e.g., "1.5M" or "1500000" (File size)
}
This interface guarantees that when we get a list of files, each item in the list will be an object with name
, type
, mtime
, and size
properties, and they will all be string
s. This is important because it tells us exactly what information is available for each file and in what format.
Gallery Data (GalleryImage
and Gallery
)#
The application also interacts with an API to fetch a gallery of images. The structure of a single image entry and the overall gallery response are defined by interfaces in src/types/gallery.ts
:
// src/types/gallery.ts
export interface GalleryImage {
id: number;
gallery_id: string;
filename: string;
resized_filename: string;
// ... many other string properties for exif data ...
exif_data_latitude: string;
exif_data_longitude: string;
exif_data_altitude: string;
// ... and potentially other types or nulls ...
weather_description: null;
photo:string; // This might be a URL or base64 data
}
export interface Gallery {
success: boolean;
http_code: number;
data: {
images: {
data: GalleryImage[]; // An array of GalleryImage objects
};
};
}
Self-correction: The GalleryImage
interface is quite long. I should simplify it significantly in the code block shown to avoid overwhelming a beginner, but explain that it contains many details defined by the interface.
Let's simplify GalleryImage
for the example code block:
// src/types/gallery.ts (Simplified for tutorial)
export interface GalleryImage {
id: number;
filename: string;
exif_data_latitude: string;
exif_data_longitude: string;
photo: string; // e.g. URL to the image
// ... many other properties defined in the actual file
}
export interface Gallery {
success: boolean;
http_code: number;
data: {
images: {
data: GalleryImage[]; // An array of objects matching the GalleryImage interface
};
};
}
These interfaces show the expected structure when fetching gallery data. Gallery
defines the top-level response (should have success
, http_code
, and a data
object containing images
, which in turn contains an array data
of GalleryImage
objects). GalleryImage
defines the structure of each item in that array, including numbers and strings for IDs, filenames, EXIF data (even if stored as strings initially), and a photo
URL. This is crucial for the code that fetches and processes gallery data (Chapter 7) and the component that displays it (Chapter 1, MapShowcaseView
) to know exactly how to access the image data and its associated metadata.
Map View State (MultiviewMapViewState
)#
In Chapter 3: 3D Geographic Visualization, we discussed different camera views (map
, firstPerson
). The state that holds the parameters (like longitude, latitude, zoom, pitch, bearing) for these different views is structured using the MultiviewMapViewState
interface in src/types/map-view-state.ts
:
// src/types/map-view-state.ts
export interface MultiviewMapViewState {
mapView: Record<string, any>; // Parameters for the map/orthographic view
firstPersonView: Record<string, any>; // Parameters for the first-person view
}
This interface specifies that the view state object should have two properties: mapView
and firstPersonView
. Each of these is defined as Record<string, any>
, which is a TypeScript way of saying "an object where the keys are strings, and the values can be anything". This is used here because the exact properties within mapView
and firstPersonView
(like longitude
, latitude
, zoom
, pitch
, bearing
, position
) are numerous and can vary slightly depending on how Deck.gl and Mapbox represent them. While Record<string, any>
is less strict than listing every single property, it still enforces that the object has these two main keys, mapView
and firstPersonView
, which is helpful for organizing the state.
GeoJSON Structure (Coords
, FileContents
)#
Geographic data, especially GeoJSON, has its own standard structure. While we don't define the entire GeoJSON standard as interfaces, we might define parts relevant to our usage, like coordinate arrays, as seen in src/types/file.ts
:
// src/types/file.ts
export type Coords = [number, number]; // A tuple for [longitude, latitude]
export interface FileContents {
// This interface might represent a simplified view of GeoJSON data
type: string; // e.g., "FeatureCollection" or "Feature"
coordinates: Coords[][][]; // Example structure for a complex polygon
// ... potentially other GeoJSON properties like 'features'
}
This interface shows how we might model parts of a GeoJSON file. Coords
is defined as a specific type alias for an array that must contain exactly two numbers (longitude and latitude). FileContents
provides a simplified model, suggesting it would have a type
string and a potentially nested coordinates
structure made up of Coords
. This helps functions processing GeoJSON understand the expected nested array structure for coordinates.
How Data Models (Interfaces) are Used in Code#
Now that we've seen some blueprints, how does TypeScript use them?
When you declare a variable or state in TypeScript, you can tell it what shape of data to expect using an interface:
// Inside MainView (simplified)
import { Metrics } from "../types/metrics";
import { MultiviewMapViewState } from "../types/map-view-state";
import { GalleryImage } from "../types/gallery";
import { UserInputs } from "../types/user-inputs";
import { NginxFile } from "../types/nginx";
// State variables declared with their expected interface type
const [metrics, setMetrics] = useState<Metrics | null>(null); // Expects Metrics object or null
const [viewState, setViewState] = useState<MultiviewMapViewState>(/* initial value */); // Expects MultiviewMapViewState
const [selectedImg, setSelectedImg] = useState<File | null | undefined>(undefined); // Expects File, null, or undefined
const [lazFile, setLazFile] = useState<NginxFile | null>(null); // Expects NginxFile object or null
// Function parameters and return types
function calculateMetrics(data: any): Metrics { // This function is declared to RETURN a Metrics object
// ... calculation logic ...
const computedMetrics: Metrics = { // We are creating an object that MUST match the Metrics interface
landArea: 100,
buildingArea: 80,
volume: 240,
buildingHeight: 3,
};
return computedMetrics; // TypeScript checks if computedMetrics matches the Metrics interface
}
// Component Props
interface BuildingAttributesProps {
metrics: Metrics; // This prop MUST be a Metrics object
lazFile: NginxFile | null; // This prop MUST be an NginxFile or null
onLazChange: (url: NginxFile) => void; // This prop MUST be a function that takes an NginxFile and returns nothing
tags: Record<string, any>; // This prop is an object with string keys and any values (less strict, but still a type)
// ... other props ...
}
// ... elsewhere, when using the component ...
<BuildingAttributes
metrics={myMetricsData} // TypeScript checks if myMetricsData is a Metrics object
lazFile={currentLazFile} // TypeScript checks if currentLazFile is an NginxFile or null
onLazChange={handleLazChange} // TypeScript checks if handleLazChange is a function that accepts an NginxFile
tags={extractedTags} // TypeScript checks if extractedTags is an object
/>
In these examples:
useState<Metrics | null>(null)
means themetrics
state variable will either hold an object that looks exactly like theMetrics
interface or benull
. If you try to set it to a string or a number, TypeScript will flag an error.function calculateMetrics(data: any): Metrics
declares that this function will return an object matching theMetrics
interface. Inside the function, when you create thecomputedMetrics
object, adding: Metrics
tells TypeScript to verify thatcomputedMetrics
has all the required properties with the correct types before the code even runs.- The
BuildingAttributesProps
interface defines the contract for the props that theBuildingAttributes
component expects. When you use<BuildingAttributes />
, TypeScript checks if you are passing props that match this interface. For example, if you tried to pass a number to themetrics
prop instead of aMetrics
object, TypeScript would give you an error.
This static type checking provided by TypeScript based on these interfaces is incredibly valuable. It helps catch errors early in the development process, preventing unexpected issues when the application is running.
Data Models as Documentation#
Beyond helping TypeScript catch errors, interfaces are excellent documentation. If you want to know what kind of data is used for metrics, you just look at the Metrics
interface. If you want to know the structure of an item in the gallery API response, you look at the GalleryImage
interface. This makes it much easier for developers to understand the codebase and how different parts of the application interact.
Conclusion#
In this chapter, we learned about Data Models, which in our TypeScript project are defined using interfaces. These interfaces act as blueprints or contracts, specifying the expected structure and types of data objects used throughout the application β for metrics, user inputs, file information, API responses, and view state. We saw how using interfaces provides clarity, prevents bugs through static type checking, ensures consistency, and serves as valuable documentation. By defining these models, we give our application's components and functions a shared understanding of the data they are working with, making the codebase more robust and easier to manage.
Now that we understand the format of the data, the next chapter will explore how we actually get some of this data into our application by Fetching Data from APIs.
Next Chapter: API Data Fetching
Generated by AI Codebase Knowledge Builder. References: 1(https://github.com/buildvoc/mapbox-gl_deck.gl_turf.js-ts/blob/3d8a4a53d878db3324af6466e0f99e5fb072bbe7/src/types/file.ts), 2(https://github.com/buildvoc/mapbox-gl_deck.gl_turf.js-ts/blob/3d8a4a53d878db3324af6466e0f99e5fb072bbe7/src/types/gallery.ts), 3(https://github.com/buildvoc/mapbox-gl_deck.gl_turf.js-ts/blob/3d8a4a53d878db3324af6466e0f99e5fb072bbe7/src/types/map-view-state.ts), 4(https://github.com/buildvoc/mapbox-gl_deck.gl_turf.js-ts/blob/3d8a4a53d878db3324af6466e0f99e5fb072bbe7/src/types/metric.ts), 5(https://github.com/buildvoc/mapbox-gl_deck.gl_turf.js-ts/blob/3d8a4a53d878db3324af6466e0f99e5fb072bbe7/src/types/metrics.ts), 6(https://github.com/buildvoc/mapbox-gl_deck.gl_turf.js-ts/blob/3d8a4a53d878db3324af6466e0f99e5fb072bbe7/src/types/nginx.ts), 7(https://github.com/buildvoc/mapbox-gl_deck.gl_turf.js-ts/blob/3d8a4a53d878db3324af6466e0f99e5fb072bbe7/src/types/user-inputs.ts)