Skip to main content
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

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_azimuth_degrees
number
required
Camera rotation angle in degrees. -15° = slight counter-clockwise rotation
camera_elevation_degrees
number
required
Camera elevation angle in degrees. -45° = isometric view
width_px
number
required
Quadrant width in pixels (1024px)
height_px
number
required
Quadrant height in pixels (1024px)
view_height_meters
number
required
Real-world height in meters covered by each quadrant (300m)
tile_step
number
required
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;
}
jobTypes
Set<string>
required
Set of selected job type codes (e.g., new Set(['NB', 'GC', 'PL']))
boroughs
Set<string>
required
Set of selected borough names (e.g., new Set(['MANHATTAN', 'BROOKLYN']))
daysBack
number
required
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
}

Build docs developers (and LLMs) love