Typing patterns in TypeScript directly impact technical debt—the accumulation of suboptimal code (hacks) that slows development, increases error risks, and raises maintenance costs. A thoughtful choice of pattern minimizes these issues, ensuring code predictability and scalability, which speeds up onboarding for new developers and reduces debugging time. Below are the base types used for further demonstration.
// Define possible segment types as a const array for strict typing
const segmentTypes = [
'line',
'quadratic',
] as const;
type SegmentType = typeof segmentTypes[number]; // Union type: 'line' | 'quadratic'
// Conditional type for coordinates, depending on segment type (discriminated by T)
type SegmentCoords<T extends SegmentType> =
T extends 'line' ? [x: number, y: number] : // For 'line' — two coordinates
T extends 'quadratic' ? [controlX: number, controlY: number, x: number, y: number] : never; // For 'quadratic' — four coordinates
These definitions serve as the foundation for four PathSegment
typing variants, which I’ll break down below.
Overview of Variants
Variant 1: Mapped Types with Indexed Access
This approach uses mapped types to create an object where keys are segment types and values are structures with corresponding type
and coords
. Indexed access forms a discriminated union. It leverages TypeScript’s declarative mapping, where types are generated automatically from the base union, ensuring a strict relationship between type
and coords
without duplication.
// Mapped type: for each T in SegmentType, creates an object { type: T; coords: SegmentCoords<T> }
type PathSegment1 = {
[T in SegmentType]: { // Iterates over SegmentType union
type: T; // Discriminant: string literal for type narrowing
coords: SegmentCoords<T>; // Coordinates dependent on T
};
}[SegmentType]; // Indexed access: union of all mapped type values
Type safety is achieved through full type narrowing: TypeScript automatically validates coords
based on type
, preventing errors like passing four coordinates for 'line'. Readability suffers due to the nested syntax of mapped types, which requires understanding advanced TypeScript features, but this pays off in large projects where automatic type generation simplifies scaling when adding new values to the base union. Compilation performance may slow with many types due to recursive mapping, but runtime is unaffected.
From a management perspective, this pattern reduces technical debt in long-term projects by minimizing type errors, but in teams with juniors, it may cause friction, leading to frustration and quick fixes like using any
.
Primary Metrics | Rating (Comment) |
---|---|
Type Safety | Very High (Full type narrowing) |
Readability | Medium (Nested syntax) |
Scalability | High (Auto-generated union) |
Secondary Metrics | Rating (Comment) |
---|---|
Compilation Performance | Medium (Mapping slows compilation) |
Runtime Performance | Very High (No overhead) |
Frustration for Juniors | Medium (Requires advanced knowledge) |
Variant 2: Record Utility Type
This approach uses the Record
utility to create an object type with keys from SegmentType
and values as generic structures, but with coords
depending on SegmentType
(union). The union is extracted via keyof
. It’s similar to mapped types from Variant 1, but Record
simplifies declarative typing at the cost of strictness, as it doesn’t ensure precise correspondence between specific keys and their values, allowing type unions without strict narrowing.
// Record: object with SegmentType keys and {type: SegmentType; coords: SegmentCoords<SegmentType>} values
type PathSegmentRecord = Record<SegmentType, { // Keys: 'line' | 'quadratic'
type: SegmentType; // Union for type, no strict narrowing within Record
coords: SegmentCoords<SegmentType>; // Union coords, less strict
}>;
type PathSegment2 = PathSegmentRecord[keyof PathSegmentRecord]; // Union of Record values
Type safety is lower than in mapped types due to the union in coords
within Record
, which reduces narrowing strictness without additional code. Readability improves thanks to the familiar Record
utility, but the two-step process (Record
+ keyof
) complicates comprehension. Scalability is effective since changes in segmentTypes
are automatically reflected, but the growing union in coords
increases risks. Compilation is fast for small type sets, and runtime has no overhead.
For management, this reduces technical debt in mixed-skill teams, but frequent type changes may lead to incorrect data due to looser typing, resulting in post-compilation errors and temporary fixes like extra type checks. Juniors may struggle with keyof
, perceiving it as "magic," which increases frustration.
Primary Metrics | Rating (Comment) |
---|---|
Type Safety | High (Narrowing via union) |
Readability | Medium (Two-step typing) |
Scalability | High (Depends on segmentTypes) |
Secondary Metrics | Rating (Comment) |
---|---|
Compilation Performance | High (Simple utility) |
Runtime Performance | Very High (Pure typing) |
Frustration for Juniors | High (keyof is complex for juniors) |
Variant 3: Generic Type with Default
This approach uses generics with a default parameter, where PathSegment
is a parameterized type. The default T=SegmentType
creates a union. It emphasizes the flexibility of generics, where type narrowing occurs naturally through conditional types in SegmentCoords
, without an explicit union.
// Generic: T constrained to SegmentType, default is union
type PathSegment5<T extends SegmentType = SegmentType> = { // Parameterized, default union
type: T; // Discriminant with generic T
coords: SegmentCoords<T>; // Dependent coords
};
Type safety is high due to generics enabling narrowing, but it requires explicit specification of T
. Readability is good due to the simplicity of generics, familiar to many developers. Adding types to segmentTypes
expands the default union, enabling efficient scaling. Compilation is fast, and runtime has no overhead.
For management, this is beneficial for quick developer onboarding, reducing technical debt. Juniors are less frustrated since generics are a fundamental concept.
Primary Metrics | Rating (Comment) |
---|---|
Type Safety | High (Narrowing via generics) |
Readability | High (Simple generics) |
Scalability | High (Default union) |
Secondary Metrics | Rating (Comment) |
---|---|
Compilation Performance | High (Fast generics) |
Runtime Performance | Very High (No overhead) |
Frustration for Juniors | Medium (Familiar generics) |
Variant 4: Interface Inheritance
This method uses a base interface with type
, then extends it to override type
and coords
. A union combines all variants. It emphasizes an OOP-like inheritance approach in TypeScript, where explicit interfaces ensure clear type narrowing via the discriminant.
// Base: shared type
interface BaseSegment {
type: SegmentType; // Union discriminant
}
// Line: extends with override type and specific coords
interface LineSegment extends BaseSegment {
type: 'line'; // Literal for narrowing
coords: [x: number, y: number]; // Two coordinates
}
// Quadratic: similarly
interface QuadraticSegment extends BaseSegment {
type: 'quadratic'; // Literal
coords: [controlX: number, controlY: number, x: number, y: number]; // Four coordinates
}
type PathSegment4 = LineSegment | QuadraticSegment; // Explicit union
Type safety is maximal due to explicit type narrowing. Readability is high thanks to the familiar interface syntax. Scaling requires adding new interfaces, which is predictable but labor-intensive. Compilation is efficient, and runtime is ideal.
For management, this minimizes technical debt since juniors are comfortable with interfaces as an even more fundamental concept than generics, but scaling with new interfaces may lead to duplication and merge conflicts in larger teams.
Primary Metrics | Rating (Comment) |
---|---|
Type Safety | Very High (Explicit narrowing) |
Readability | Very High (Familiar interfaces) |
Scalability | High (New interfaces required) |
Secondary Metrics | Rating (Comment) |
---|---|
Compilation Performance | High (Simple interfaces) |
Runtime Performance | Very High (Pure typing) |
Frustration for Juniors | Low (Basic interfaces) |
Technical Comparison Table
Variant | Pattern | Type Safety | Readability | Scalability | Total Score |
---|---|---|---|---|---|
1 | Mapped Types with Indexed Access | Very High | Medium | High | 12 |
2 | Record Utility Type | High | Medium | High | 10 |
3 | Generic Discriminated Union | High | High | High | 12 |
4 | Interface Inheritance | Very High | Very High | High | 14 |
Comparative Analysis
Type Safety: Variants 4 (Interface Inheritance) and 1 (Mapped Types with Indexed Access) are the best due to strict type narrowing. Variant 4 uses explicit interfaces with specific literals, eliminating errors in type
and coords
correspondence. Variant 1 achieves the same through mapped types, automatically linking type
with SegmentCoords<T>
.
Readability: Variant 4 (Interface Inheritance) is the best due to its OOP-like interface syntax, which is intuitive even for complete beginners.
Scalability: Variants 1 (Mapped Types with Indexed Access), 2 (Record Utility Type), and 3 (Generic Discriminated Union) automatically adapt to changes in segmentTypes
due to their declarative nature (mapped types, Record, default union). Variant 4 requires manual effort to add new interfaces.
Recommendations for Application
For projects with predominantly junior developers, Variant 4 (Interface Inheritance) is the best fit—it minimizes frustration and technical debt. In teams with experienced developers, Variant 1 (Mapped Types with Indexed Access) is preferable for its automatic scalability, while Variant 3 (Generic Type with Default) suits those who prefer generics. Variant 2 is less ideal, as utility type workarounds can lead to issues.
I personally use Variant 1 (Mapped Types with Indexed Access) because it’s a true powerhouse. I’m not in a rush, so compilation speed isn’t a concern. If someone on the team doesn’t understand it, I’ll just play them an infinite loop of "Come on! You can do it! ❤️ Believe in yourself 🙏 Sweetie 🥺 Believe 💘 Come on, come on!! Push a little harder ☝🏼 Just a bit more... please don’t give up 😕 Keep going 😘 Push harder 😉 You’ve got this!! 😭".
Top comments (0)