TfL API TypeScript Client
A fully-typed TypeScript client for the Transport for London (TfL) API with auto-generated types, real-time data support, and comprehensive coverage of all TfL endpoints. Built with modern TypeScript practices and zero dependencies.
v0.0.1
🍰
🏃
🎓
package
TfL API TypeScript Client
A fully-typed TypeScript client for the Transport for London (TfL) API with auto-generated types, real-time data support, and comprehensive coverage of all TfL endpoints. Built with modern TypeScript practices and zero dependencies.
Description
Please visit the npm package for more details.
Basic usage:
import TfLClients from "tfl-ts";
const tfl = new TfLClients();
const tubeStatus = await tfl.line.getStatus({ modes: ['tube'] });
Example:
Line Status
Bakerloo
Minor DelaysNo service between Harrow & Wealdstone and Queen's Park while Network Rail fix a signal failure in the Willesden Junction area. Tickets are being accepted on London Buses. MINOR DELAYS on the rest of the line.
Part SuspendedNo service between Harrow & Wealdstone and Queen's Park while Network Rail fix a signal failure in the Willesden Junction area. Tickets are being accepted on London Buses. MINOR DELAYS on the rest of the line.
Jubilee
Severe DelaysSevere delays due to an earlier signal failure at Stratford. Tickets are being accepted on London Buses, London Overground, the DLR, the Elizabeth line, C2C, Southeastern and Thameslink.
Lioness
Severe DelaysSevere delays while Network Rail fix a signal failure in the Willesden Junction area. Tickets are being accepted on London Buses.
Northern
Minor DelaysMinor delays due to an earlier points failure at Golders Green.
Piccadilly
Minor DelaysMinor delays due to an earlier signal failure at Arnos Grove.
District
Minor DelaysMinor delays due to an earlier fire alert at Barking and Network Rail signal failure in the Parsons Green area.
DLR
Minor DelaysMinor delays between Stratford International and Canning Town due to an earlier faulty train at West Ham. GOOD SERVICE on the rest of the line.
Good Service on all other lines
Central
Victoria
Circle
Hammersmith & City
Metropolitan
Waterloo & City
Tram
Elizabeth line
Liberty
Mildmay
Suffragette
Weaver
Windrush
import { type ReactNode } from "react";
import Link from "next/link";
import TfLClients from "tfl-ts";
import { ExternalLink, TrainFrontTunnel } from 'lucide-react';
import { cn } from '@workspace/ui/lib/utils';
interface Props {
children?: ReactNode;
}
const TFLLogo = ({ className }: { className?: string }) => (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
className={cn(className, "dark:brightness-200")}
>
<svg
style={{
width: '100%',
height: '100%'
}}
viewBox="0 0 615.322 500"
>
<g>
<path
style={{
fill: '#000F9F'
}}
d="M469.453,249.986c0,89.078-72.26,161.308-161.337,161.308c-89.1,0-161.294-72.23-161.294-161.308 c0-89.063,72.194-161.286,161.294-161.286C397.194,88.699,469.453,160.922,469.453,249.986 M308.116,0 C170.027,0,58.094,111.925,58.094,249.986C58.094,388.06,170.027,500,308.116,500c138.06,0,249.985-111.94,249.985-250.014 C558.101,111.925,446.176,0,308.116,0"
/>
<rect
style={{
fill: '#000F9F'
}}
y="199.516"
width="615.322"
height="101.129"
/>
</g>
</svg>
</div>
);
const lineColorMap = {
'bakerloo': { text: 'text-[#B36305]', bg: 'bg-[#B36305]', badWithDarkBg: false },
'central': { text: 'text-[#E32017]', bg: 'bg-[#E32017]', badWithDarkBg: false },
'circle': { text: 'text-[#FFD300]', bg: 'bg-[#FFD300]', badWithDarkBg: false },
'district': { text: 'text-[#00782A]', bg: 'bg-[#00782A]', badWithDarkBg: false },
'hammersmith-city': { text: 'text-[#F3A9BB]', bg: 'bg-[#F3A9BB]', badWithDarkBg: false },
'jubilee': { text: 'text-[#A0A5A9]', bg: 'bg-[#A0A5A9]', badWithDarkBg: false },
'metropolitan': { text: 'text-[#9B0056]', bg: 'bg-[#9B0056]', badWithDarkBg: false },
'northern': { text: 'text-[#000000]', bg: 'bg-[#000000]', badWithDarkBg: true },
'piccadilly': { text: 'text-[#003688]', bg: 'bg-[#003688]', badWithDarkBg: false },
'victoria': { text: 'text-[#0098D4]', bg: 'bg-[#0098D4]', badWithDarkBg: false },
'waterloo-city': { text: 'text-[#95CDBA]', bg: 'bg-[#95CDBA]', badWithDarkBg: false },
'dlr': { text: 'text-[#00A4A7]', bg: 'bg-[#00A4A7]', badWithDarkBg: false },
'elizabeth': { text: 'text-[#6950A1]', bg: 'bg-[#6950A1]', badWithDarkBg: false },
'tram': { text: 'text-[#5fb526]', bg: 'bg-[#5fb526]', badWithDarkBg: false },
'liberty': { text: 'text-[#0071FD]', bg: 'bg-[#0071FD]', badWithDarkBg: false },
'lioness': { text: 'text-[#FC9D9A]', bg: 'bg-[#FC9D9A]', badWithDarkBg: false },
'mildmay': { text: 'text-[#0071FD]', bg: 'bg-[#0071FD]', badWithDarkBg: false },
'suffragette': { text: 'text-[#76B82A]', bg: 'bg-[#76B82A]', badWithDarkBg: false },
'weaver': { text: 'text-[#A45A2A]', bg: 'bg-[#A45A2A]', badWithDarkBg: false },
'windrush': { text: 'text-[#EE2E24]', bg: 'bg-[#EE2E24]', badWithDarkBg: false }
} as const;
const tfl = new TfLClients();
// Get severity data from TfL API
const severity = tfl.line.ALL_SEVERITY.filter(mode => mode.modeName === 'tube');
// Create dynamic severity mappings based on TfL data
const createSeverityMapping = () => {
const critical: number[] = [];
const severe: number[] = [];
const minor: number[] = [];
const special: number[] = [];
const good: number[] = [];
severity.forEach(item => {
const level = item.severityLevel;
const description = item.description.toLowerCase();
if (description.includes('closed') || description.includes('suspended') || description.includes('not running')) {
critical.push(level);
} else if (description.includes('severe') || description.includes('part closure') || description.includes('exit only')) {
severe.push(level);
} else if (description.includes('minor') || description.includes('reduced') || description.includes('bus service') || description.includes('diverted') || description.includes('issues')) {
minor.push(level);
} else if (description.includes('special') || description.includes('no step free') || description.includes('information')) {
special.push(level);
} else if (description.includes('good') || description.includes('no issues') || description.includes('closed for the night')) {
good.push(level);
}
});
return {
critical,
severe,
minor,
special,
good
};
};
const severityMapping = createSeverityMapping();
// Create a union type from all severity levels
type SeverityNumber = number;
const isNormalSeverity = (statuses: Array<{ statusSeverity?: number }>) => {
return statuses.every(status => {
const severity = status.statusSeverity;
if (severity === undefined) return false;
return severityMapping.good.includes(severity) ||
severityMapping.special.includes(severity);
});
};
const getSeverityClass = (severity: SeverityNumber) => {
if (severityMapping.critical.includes(severity)) return 'text-red-700 animate-[pulse_1s_ease-in-out_infinite]';
if (severityMapping.severe.includes(severity)) return 'text-orange-700 animate-[pulse_1.5s_ease-in-out_infinite]';
if (severityMapping.minor.includes(severity)) return 'text-yellow-700 animate-[pulse_2s_ease-in-out_infinite]';
if (severityMapping.special.includes(severity)) return 'text-blue-700';
return 'text-green-700';
};
const getSeverityAnimation = (severity: SeverityNumber) => {
if (severityMapping.critical.includes(severity)) return "animate-[pulse_1s_ease-in-out_infinite]";
if (severityMapping.severe.includes(severity)) return "animate-[pulse_1.5s_ease-in-out_infinite]";
if (severityMapping.minor.includes(severity)) return "animate-[pulse_2s_ease-in-out_infinite]";
return "";
};
const hasNightClosure = (statuses: Array<{ statusSeverity?: number }>) => {
return statuses.some(status => status.statusSeverity === 20);
};
const lineOrder = [
// Tube lines by passenger volume (busiest first)
'central',
'northern',
'jubilee',
'piccadilly',
'district',
'victoria',
'circle',
'hammersmith-city',
'bakerloo',
'metropolitan',
'waterloo-city',
// Other transport modes
'dlr',
'tram',
'elizabeth',
// Overground group
'liberty',
'lioness',
'mildmay',
'suffragette',
'weaver',
'windrush'
] as const;
const getLineOrder = (lineId: string) => {
const index = lineOrder.indexOf(lineId as typeof lineOrder[number]);
return index === -1 ? lineOrder.length : index;
};
export async function TflTs(props: Props) {
// Get status for multiple modes
const lineStatuses = await tfl.line.getStatus({ modes: ['tube', 'elizabeth-line', 'dlr', 'tram', 'overground'] });
// Enhanced sorting logic
const sortedLineStatuses = lineStatuses.sort((a, b) => {
const aMinSeverity = Math.min(...(a.lineStatuses?.map(s => s.statusSeverity || 0) || []));
const bMinSeverity = Math.min(...(b.lineStatuses?.map(s => s.statusSeverity || 0) || []));
// If both lines have normal service, sort by predefined order
if (isNormalSeverity(a.lineStatuses || []) && isNormalSeverity(b.lineStatuses || [])) {
return getLineOrder(a.id || '') - getLineOrder(b.id || '');
}
// If severities are different, sort by severity
if (aMinSeverity !== bMinSeverity) {
return aMinSeverity - bMinSeverity;
}
// If both lines have the same severity level (but not normal),
// still sort by predefined order as a fallback
return getLineOrder(a.id || '') - getLineOrder(b.id || '');
});
return (
<div className="w-full mt-4 flex flex-col gap-2">
<div className="flex items-center gap-2 justify-between flex-wrap">
<div className="flex items-center gap-2">
<TFLLogo className="size-[var(--text-4xl)] lg:size-[var(--text-5xl)]" />
<h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">
Line Status
</h1>
</div>
<div className="flex items-center gap-2"><Link href="https://tfl.gov.uk/tube-dlr-overground/status/" className="text-blue-500 hover:underline">TfL website <ExternalLink className="size-4 inline-block" /></Link></div>
</div>
{/* Lines with issues */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{sortedLineStatuses.filter(line => !isNormalSeverity(line.lineStatuses || [])).map((line) => (
<div
key={line.id}
className="flex flex-col gap-0"
>
<h2 className="text-lg font-semibold">{line.name}</h2>
<div className="w-full h-[6px] relative">
{(line.modeName === 'overground' || line.modeName === 'elizabeth-line') ? (
<>
<div className={`w-full h-[6px] ${line.id && line.id in lineColorMap
? `${lineColorMap[line.id as keyof typeof lineColorMap].bg}`
: 'bg-gray-400'
} ${line.id && line.id in lineColorMap && lineColorMap[line.id as keyof typeof lineColorMap].badWithDarkBg ? 'dark:shadow-[0_0_8px_rgba(255,255,255,0.3)]' : ''}`} />
<div className="absolute top-[2px] left-0 w-full h-[2px] bg-white" />
</>
) : (
<div className={`w-full h-[6px] ${line.id && line.id in lineColorMap
? `${lineColorMap[line.id as keyof typeof lineColorMap].bg}`
: 'bg-gray-400'
} ${line.id && line.id in lineColorMap && lineColorMap[line.id as keyof typeof lineColorMap].badWithDarkBg ? 'dark:shadow-[0_0_8px_rgba(255,255,255,0.3)]' : ''}`} />
)}
</div>
{line.lineStatuses?.map((status, index) => {
const severityTitleClass = getSeverityClass(status.statusSeverity as SeverityNumber);
const statusDescription = status.statusSeverityDescription;
return (
<div
key={index}
className={`mt-2`}
>
<div className="">
<span className={`font-medium ${severityTitleClass} mr-2`}>
{statusDescription}
</span>
{status.reason && (
<span className={`text-sm text-foreground/80 mt-1 text-pretty`}>
{status.reason?.replace(new RegExp(`^${line.name?.toUpperCase()}( LINE)?: `, 'i'), '').replace(/^(Hammersmith and City Line: )|(London Overground: )|(Docklands Light Railway: )\s*/, '')}
</span>
)}
</div>
</div>
);
})}
</div>
))}
</div>
{/* Lines with good service */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 justify-items-stretch">
<div className="flex flex-col justify-end col-span-2 md:col-span-3 lg:col-span-1">
<h2 className="text-xl prose font-semibold leading-none">Good Service on all {sortedLineStatuses.filter(line => !isNormalSeverity(line.lineStatuses || [])).length > 0 && `other `}lines</h2>
</div>
{sortedLineStatuses.filter(line => isNormalSeverity(line.lineStatuses || [])).map((line) => (
<div
key={line.id}
className="flex flex-col"
>
<div className="flex justify-between">
<h2 className="text-lg font-semibold">
{line.name}
</h2>
{hasNightClosure(line.lineStatuses || []) && (
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<TrainFrontTunnel className={`w-3 h-3 ${line.id && line.id in lineColorMap ? `${lineColorMap[line.id as keyof typeof lineColorMap].text}` : 'text-gray-400'}`} />
<span>
{line.lineStatuses?.find(status => status.statusSeverity === 20)?.statusSeverityDescription}
</span>
</div>
)}
</div>
<div className="w-full h-[6px] relative">
{(line.modeName === 'overground' || line.modeName === 'elizabeth-line') ? (
<>
<div className={`w-full h-[6px] ${line.id && line.id in lineColorMap
? `${lineColorMap[line.id as keyof typeof lineColorMap].bg}`
: 'bg-gray-400'
} ${line.id && line.id in lineColorMap && lineColorMap[line.id as keyof typeof lineColorMap].badWithDarkBg ? 'dark:shadow-[0_0_8px_rgba(255,255,255,0.3)]' : ''}`} />
<div className="absolute top-[2px] left-0 w-full h-[2px] bg-white" />
</>
) : (
<div className={`w-full h-[6px] ${line.id && line.id in lineColorMap
? `${lineColorMap[line.id as keyof typeof lineColorMap].bg}`
: 'bg-gray-400'
} ${line.id && line.id in lineColorMap && lineColorMap[line.id as keyof typeof lineColorMap].badWithDarkBg ? 'dark:shadow-[0_0_8px_rgba(255,255,255,0.3)]' : ''}`} />
)}
</div>
</div>
))}
</div>
{props.children}
</div>
);
}