Type Safety Reference¶
Complete guide to TypeScript patterns and type safety practices in FiberPath GUI.
Overview¶
FiberPath GUI uses TypeScript strict mode for maximum type safety. This reference covers patterns for discriminated unions, type guards, error handling, and Zod integration.
Discriminated Unions¶
Layer Types¶
// Base type
type BaseLayer = {
id: string;
terminal: boolean;
skipEvery?: number;
};
// Specific layer types
type HoopLayer = BaseLayer & {
windType: "hoop";
};
type HelicalLayer = BaseLayer & {
windType: "helical";
windAngle: number;
};
// Union type with discriminator
type Layer = HoopLayer | HelicalLayer;
Discriminated Field: windType uniquely identifies layer variant.
Type Guards¶
function isHoopLayer(layer: Layer): layer is HoopLayer {
return layer.windType === "hoop";
}
function isHelicalLayer(layer: Layer): layer is HelicalLayer {
return layer.windType === "helical";
}
// Usage
function getLayerInfo(layer: Layer): string {
if (isHoopLayer(layer)) {
return `Hoop layer (terminal: ${layer.terminal})`;
// TypeScript knows layer is HoopLayer (no windAngle)
} else if (isHelicalLayer(layer)) {
return `Helical layer (angle: ${layer.windAngle}°)`;
// TypeScript knows layer is HelicalLayer (has windAngle)
} else {
// Exhaustiveness check
const _exhaustive: never = layer;
throw new Error(`Unknown layer type: ${_exhaustive}`);
}
}
Exhaustive Switch¶
function processLayer(layer: Layer): void {
switch (layer.windType) {
case "hoop":
console.log("Processing hoop layer");
break;
case "helical":
console.log(`Processing helical layer at ${layer.windAngle}°`);
break;
default:
// If we add a new layer type and forget to handle it,
// TypeScript will error here
const _exhaustive: never = layer;
throw new Error(`Unhandled layer type: ${_exhaustive}`);
}
}
Benefit: Compiler enforces handling all cases.
Zod Schema Integration¶
Type Inference¶
import { z } from "zod";
// Define schema
export const MandrelParametersSchema = z.object({
diameter: z.number().positive(),
windLength: z.number().positive(),
});
// Infer TypeScript type from schema
export type MandrelParameters = z.infer<typeof MandrelParametersSchema>;
// Equivalent to:
// type MandrelParameters = {
// diameter: number;
// windLength: number;
// };
Benefit: Single source of truth - runtime and compile-time validation aligned.
Runtime Validation with Type Safety¶
function validateMandrel(data: unknown): MandrelParameters {
const result = MandrelParametersSchema.safeParse(data);
if (!result.success) {
throw new ValidationError(
"Invalid mandrel parameters",
result.error.issues
);
}
return result.data; // Type: MandrelParameters
}
Partial Updates¶
// Schema for full object
export const LayerSchema = z.object({
id: z.string(),
windType: z.enum(["hoop", "helical"]),
terminal: z.boolean(),
skipEvery: z.number().int().positive().optional(),
});
// Schema for partial updates (all fields optional)
export const PartialLayerSchema = LayerSchema.partial();
export type Layer = z.infer<typeof LayerSchema>;
export type PartialLayer = z.infer<typeof PartialLayerSchema>;
// Usage
function updateLayer(id: string, updates: PartialLayer): void {
// updates can be { terminal: true } or { skipEvery: 2 } etc.
}
Error Handling¶
Custom Error Classes¶
export class CommandError extends Error {
constructor(
message: string,
public command: string,
public cause?: unknown
) {
super(message);
this.name = "CommandError";
}
}
export class ValidationError extends Error {
constructor(
message: string,
public errors: Array<{ field: string; message: string }>
) {
super(message);
this.name = "ValidationError";
}
}
export class FileError extends Error {
constructor(
message: string,
public filePath: string,
public cause?: unknown
) {
super(message);
this.name = "FileError";
}
}
export class ConnectionError extends Error {
constructor(
message: string,
public port: string,
public cause?: unknown
) {
super(message);
this.name = "ConnectionError";
}
}
Type-Safe Error Handling¶
async function executePlan(inputPath: string): Promise<PlanSummary> {
try {
return await planWind(inputPath);
} catch (error) {
if (error instanceof CommandError) {
console.error(`Command ${error.command} failed: ${error.message}`);
throw error;
} else if (error instanceof ValidationError) {
console.error("Validation failed:", error.errors);
throw error;
} else if (error instanceof FileError) {
console.error(`File error at ${error.filePath}: ${error.message}`);
throw error;
} else {
// Unknown error
console.error("Unexpected error:", error);
throw new Error("An unexpected error occurred");
}
}
}
Result Type Pattern¶
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
function safePlanWind(inputPath: string): Promise<Result<PlanSummary>> {
return planWind(inputPath)
.then((data) => ({ success: true as const, data }))
.catch((error) => ({ success: false as const, error }));
}
// Usage
const result = await safePlanWind(inputPath);
if (result.success) {
console.log(result.data.commands); // Type: PlanSummary
} else {
console.error(result.error); // Type: Error
}
Utility Types¶
Partial¶
type MandrelParameters = {
diameter: number;
windLength: number;
};
// All fields optional
type PartialMandrel = Partial<MandrelParameters>;
// Equivalent to: { diameter?: number; windLength?: number; }
Required¶
type OptionalConfig = {
axisFormat?: "xab" | "xyz";
dryRun?: boolean;
};
// All fields required
type RequiredConfig = Required<OptionalConfig>;
// Equivalent to: { axisFormat: "xab" | "xyz"; dryRun: boolean; }
Pick¶
type Layer = {
id: string;
windType: "hoop" | "helical";
terminal: boolean;
skipEvery?: number;
};
// Pick specific fields
type LayerSummary = Pick<Layer, "id" | "windType">;
// Equivalent to: { id: string; windType: "hoop" | "helical"; }
Omit¶
// Omit specific fields
type LayerWithoutId = Omit<Layer, "id">;
// Equivalent to: { windType: "hoop" | "helical"; terminal: boolean; skipEvery?: number; }
Exclude¶
type AxisFormat = "xab" | "xyz" | "xyzab";
// Exclude specific values from union
type SimpleAxisFormat = Exclude<AxisFormat, "xyzab">;
// Equivalent to: "xab" | "xyz"
Extract¶
type Action =
| { type: "add"; payload: Layer }
| { type: "remove"; payload: string }
| { type: "update"; payload: { id: string; changes: Partial<Layer> } };
// Extract specific variants
type AddAction = Extract<Action, { type: "add" }>;
// Equivalent to: { type: "add"; payload: Layer }
ReturnType¶
function createLayer(type: LayerType): Layer {
// ...
}
type CreatedLayer = ReturnType<typeof createLayer>;
// Equivalent to: Layer
Parameters¶
function updateMandrel(id: string, params: MandrelParameters): void {
// ...
}
type UpdateMandrelParams = Parameters<typeof updateMandrel>;
// Equivalent to: [id: string, params: MandrelParameters]
Advanced Patterns¶
Branded Types¶
// Ensure IDs are not mixed with regular strings
type LayerId = string & { __brand: "LayerId" };
type ProjectId = string & { __brand: "ProjectId" };
function createLayerId(id: string): LayerId {
return id as LayerId;
}
function removeLayer(id: LayerId): void {
// ...
}
// Usage
const layerId = createLayerId("layer-123");
removeLayer(layerId); // ✅ OK
const projectId: ProjectId = "proj-456" as ProjectId;
removeLayer(projectId); // ❌ Type error
Const Assertions¶
const config = {
axisFormat: "xab",
dryRun: false,
} as const;
// Type: { readonly axisFormat: "xab"; readonly dryRun: false; }
// vs
const config = {
axisFormat: "xab",
dryRun: false,
};
// Type: { axisFormat: string; dryRun: boolean; }
Use Case: Narrow types to literal values.
Template Literal Types¶
type CommandName = "plan" | "simulate" | "plot";
type CommandKey = `${CommandName}_command`;
// Equivalent to: "plan_command" | "simulate_command" | "plot_command"
Mapped Types¶
type LayerState = {
isEditing: boolean;
isHovered: boolean;
isSelected: boolean;
};
// Create optional version of all fields
type OptionalLayerState = {
[K in keyof LayerState]?: LayerState[K];
};
// Equivalent to:
// type OptionalLayerState = {
// isEditing?: boolean;
// isHovered?: boolean;
// isSelected?: boolean;
// };
Conditional Types¶
type IsArray<T> = T extends any[] ? true : false;
type A = IsArray<string[]>; // true
type B = IsArray<number>; // false
// More practical: Unwrap array type
type Unwrap<T> = T extends Array<infer U> ? U : T;
type C = Unwrap<Layer[]>; // Layer
type D = Unwrap<number>; // number
Type Assertions¶
As Const¶
const layers = [
{ windType: "hoop", terminal: false },
{ windType: "helical", windAngle: 45, terminal: false },
] as const;
// Type: readonly [
// { readonly windType: "hoop"; readonly terminal: false },
// { readonly windType: "helical"; readonly windAngle: 45; readonly terminal: false }
// ]
Type Casting¶
// ❌ Avoid when possible
const data = response as MandrelParameters;
// ✅ Prefer validation
const data = MandrelParametersSchema.parse(response);
Non-Null Assertion¶
// When you know value is not null
const project = useProjectStore((s) => s.project);
const diameter = project!.mandrelParameters.diameter;
// ⚠️ Dangerous: Runtime error if project is null
// ✅ Prefer optional chaining
const diameter = project?.mandrelParameters.diameter;
Type Narrowing¶
typeof¶
function processValue(value: string | number): string {
if (typeof value === "string") {
return value.toUpperCase(); // Type: string
} else {
return value.toFixed(2); // Type: number
}
}
instanceof¶
try {
await planWind(inputPath);
} catch (error) {
if (error instanceof CommandError) {
console.error(`Command failed: ${error.command}`);
} else if (error instanceof Error) {
console.error(`Error: ${error.message}`);
} else {
console.error("Unknown error:", error);
}
}
in operator¶
type Response =
| { status: "success"; data: PlanSummary }
| { status: "error"; message: string };
function handleResponse(response: Response): void {
if ("data" in response) {
console.log(response.data.commands); // Type: PlanSummary
} else {
console.error(response.message); // Type: string
}
}
Equality¶
type State = "idle" | "loading" | "success" | "error";
function handleState(state: State): void {
if (state === "loading") {
// Type: "loading"
} else if (state === "success" || state === "error") {
// Type: "success" | "error"
} else {
// Type: "idle"
}
}
Best Practices¶
✅ Do¶
- Enable strict mode in tsconfig.json
- Infer types from schemas with Zod
- Use discriminated unions for variants
- Write type guards for runtime type checks
- Prefer unknown over any for untyped data
- Use exhaustiveness checks in switches
- Document complex types with JSDoc
❌ Don't¶
- Use
any(breaks type safety) - Ignore TypeScript errors with
@ts-ignore - Over-assert with
as(validate instead) - Create overly complex types (keep types readable)
- Forget to handle all union cases
Testing Types¶
Type Tests¶
import { expectType } from "tsd";
// Assert return type
expectType<MandrelParameters>(
MandrelParametersSchema.parse({ diameter: 150, windLength: 800 })
);
// Assert discriminated union narrows correctly
const layer: Layer = { windType: "hoop", terminal: false };
if (layer.windType === "hoop") {
expectType<HoopLayer>(layer);
}
Compile-Time Tests¶
// Should compile
const validLayer: Layer = { windType: "hoop", terminal: false };
// Should NOT compile (uncomment to test)
// const invalidLayer: Layer = { windType: "unknown", terminal: false };
Resources¶
Next Steps¶
- Schema Validation Guide - Zod patterns
- State Management - Typed store
- Testing Guide - Type-safe tests