The types.ts module defines all TypeScript interfaces used throughout the application.
Interfaces
Permit
Represents a single DOB permit from the NYC Open Data API (DOB NOW: Build dataset).
export interface Permit {
// Identity fields
job_filing_number ?: string ;
work_permit ?: string ;
tracking_number ?: string ;
bin ?: string ;
sequence_number ?: string ;
// Address fields
house_no ?: string ;
street_name ?: string ;
borough ?: string ;
zip_code ?: string ;
block ?: string ;
lot ?: string ;
bbl ?: string ;
community_board ?: string ;
nta ?: string ;
council_district ?: string ;
// Work fields
work_type ?: string ;
job_type ?: string ;
permit_status ?: string ;
job_description ?: string ;
filing_reason ?: string ;
work_on_floor ?: string ;
estimated_job_costs ?: string ;
// Date fields
issued_date ?: string ;
approved_date ?: string ;
expired_date ?: string ;
// Owner fields
owner_name ?: string ;
owner_business_name ?: string ;
// Applicant / contractor fields
applicant_first_name ?: string ;
applicant_last_name ?: string ;
applicant_business_name ?: string ;
applicant_business_address ?: string ;
// Filing representative (expediter) fields
filing_representative_first_name ?: string ;
filing_representative_last_name ?: string ;
filing_representative_business_name ?: string ;
// Coordinate fields
latitude ?: string ;
longitude ?: string ;
}
All fields are optional because the DOB dataset has inconsistent data coverage. Always check for existence before accessing.
Field Groups
Unique job filing identifier (e.g., "121234567")
Internal DOB tracking number
Building Identification Number — NYC’s unique building ID
Street number (e.g., "350")
Street name (e.g., "5th Ave")
Borough name: MANHATTAN, BROOKLYN, QUEENS, BRONX, or STATEN ISLAND
Borough-Block-Lot identifier (10 digits: 1 + 5 + 4)
Neighborhood Tabulation Area name (e.g., "Fort Greene")
NYC Council district number
Verbose work type from DOB (e.g., "General Construction", "Full Demolition")
Normalized job type code: NB, DM, GC, PL, ME, SOL, SHD, SCF, FNC, SG, FND, STR, BLR, SPR, EW, ANT, CC, STP, or OTH Note : This is derived client-side via workTypeToCode() and not present in the raw API response.
Current permit status (e.g., "ISSUED", "EXPIRED")
Free-text job description from applicant
Reason for filing (e.g., "ALTERATION", "NEW BUILDING")
Floors affected by work (e.g., "1-3", "ROOF")
Estimated cost in dollars (as string, e.g., "150000")
Date permit was issued (ISO format: "2026-02-24T00:00:00.000")
Date job was approved (ISO format: "2026-02-24T00:00:00.000") Note : For Job Filings dataset, this is the primary date field.
Date permit expires (ISO format: "2026-08-24T00:00:00.000")
Business entity owner name
Show Applicant / Contractor Fields
applicant_business_address
Contractor business address
Show Filing Representative (Expediter) Fields
filing_representative_first_name
Expediter first name
filing_representative_last_name
Expediter last name
filing_representative_business_name
Expediter business name
Latitude in decimal degrees (as string, e.g., "40.748400")
Longitude in decimal degrees (as string, e.g., "-73.985700") Note : Always validate with parseFloat() and isNaN() check before use.
Example Usage
import type { Permit } from './types' ;
import { formatAddress , formatDate , getJobLabel } from './permits' ;
function displayPermit ( permit : Permit ) {
// Safe field access with fallbacks
const address = formatAddress ( permit );
const date = formatDate ( permit . issued_date ?? permit . approved_date );
const jobLabel = getJobLabel ( permit . job_type ?? 'OTH' );
const cost = permit . estimated_job_costs
? `$ ${ Number ( permit . estimated_job_costs ). toLocaleString () } `
: 'N/A' ;
console . log ( ` ${ jobLabel } - ${ address } ` );
console . log ( `Issued: ${ date } ` );
console . log ( `Estimated Cost: ${ cost } ` );
// Check for optional fields
if ( permit . job_description ) {
console . log ( `Description: ${ permit . job_description } ` );
}
// Parse coordinates
const lat = parseFloat ( permit . latitude ?? '' );
const lng = parseFloat ( permit . longitude ?? '' );
if ( ! isNaN ( lat ) && ! isNaN ( lng )) {
console . log ( `Location: ${ lat } , ${ lng } ` );
}
}
MapConfig
Configuration for the isometric map projection system.
export interface MapConfig {
seed : { lat : number ; lng : number };
camera_azimuth_degrees : number ;
camera_elevation_degrees : number ;
width_px : number ;
height_px : number ;
view_height_meters : number ;
tile_step : number ;
}
seed
{ lat: number; lng: number }
required
Geographic seed point for coordinate projection (Empire State Building area)
Camera rotation angle in degrees. -15° = slight counter-clockwise rotation
Camera elevation angle in degrees. -45° = isometric view
Quadrant width in pixels (1024px)
Quadrant height in pixels (1024px)
Real-world height in meters covered by each quadrant (300m)
Tile step multiplier. 0.5 = 512px quadrants from 1024px tiles
Example Usage
import type { MapConfig } from './types' ;
import { MAP_CONFIG } from './coordinates' ;
// Use the default config
function getMetersPerPixel ( config : MapConfig ) : number {
return config . view_height_meters / config . height_px ;
}
const mpp = getMetersPerPixel ( MAP_CONFIG );
console . log ( `Meters per pixel: ${ mpp . toFixed ( 3 ) } ` );
// Output: Meters per pixel: 0.293
FilterState
Application filter state for permit filtering.
export interface FilterState {
jobTypes : Set < string >;
boroughs : Set < string >;
daysBack : number ;
}
Set of selected job type codes (e.g., new Set(['NB', 'GC', 'PL']))
Set of selected borough names (e.g., new Set(['MANHATTAN', 'BROOKLYN']))
Number of days of permit history to fetch (typically 7 or 30)
Example Usage
import { useState , useMemo } from 'react' ;
import type { Permit , FilterState } from './types' ;
import { ALL_JOB_TYPES } from './permits' ;
function usePermitFilters ( permits : Permit []) {
const [ filters , setFilters ] = useState < FilterState >({
jobTypes: new Set ( ALL_JOB_TYPES ),
boroughs: new Set ([ 'MANHATTAN' ]),
daysBack: 7 ,
});
const filteredPermits = useMemo (() => {
return permits . filter ( permit => {
const jobType = permit . job_type ?. toUpperCase () ?? 'OTHER' ;
const borough = permit . borough ?. toUpperCase () ?? '' ;
const jobMatch = filters . jobTypes . has ( jobType );
const boroughMatch = filters . boroughs . has ( borough );
return jobMatch && boroughMatch ;
});
}, [ permits , filters ]);
// Toggle a job type
const toggleJobType = ( jt : string ) => {
setFilters ( prev => {
const next = new Set ( prev . jobTypes );
next . has ( jt ) ? next . delete ( jt ) : next . add ( jt );
return { ... prev , jobTypes: next };
});
};
// Toggle a borough
const toggleBorough = ( b : string ) => {
setFilters ( prev => {
const next = new Set ( prev . boroughs );
next . has ( b ) ? next . delete ( b ) : next . add ( b );
return { ... prev , boroughs: next };
});
};
return { filters , filteredPermits , toggleJobType , toggleBorough };
}
Data Sources
DOB NOW Datasets
The Permit interface models data from two NYC Open Data datasets:
1. DOB NOW: Build – Approved Permits (rbx6-tga4)
Work-type permits : GC, PL, Mechanical, Solar, Scaffold, etc.
Excludes : New Building and Full Demolition
Update frequency : Daily
2. DOB NOW: Build – Job Filings (w9ak-ipjd)
Job-level filings : New Building, Full Demolition, Alteration
Primary use : NB and DM permits only
Update frequency : Daily
Both datasets are normalized by fetchPermits() into a unified Permit[] array with consistent job_type codes.
Type Safety Best Practices
import type { Permit } from './types' ;
// ✓ Always check for undefined
function safeAccess ( permit : Permit ) {
const lat = parseFloat ( permit . latitude ?? '' );
const lng = parseFloat ( permit . longitude ?? '' );
if ( isNaN ( lat ) || isNaN ( lng )) {
console . warn ( 'Invalid coordinates' );
return null ;
}
return { lat , lng };
}
// ✓ Use optional chaining
function getOwner ( permit : Permit ) : string | null {
return permit . owner_business_name
?? permit . owner_name
?? null ;
}
// ✓ Type-safe filtering
function filterByBorough (
permits : Permit [],
borough : string
) : Permit [] {
return permits . filter ( p =>
p . borough ?. toUpperCase () === borough . toUpperCase ()
);
}
// ✗ Avoid assuming fields exist
function unsafeAccess ( permit : Permit ) {
// Don't do this!
const lat = parseFloat ( permit . latitude ); // Type error if undefined
const name = permit . owner_name . toUpperCase (); // Runtime error if undefined
}