DEV Community

Kurnakov Ilya
Kurnakov Ilya

Posted on • Originally published at habr.com

Mindful Selection of TypeScript Typing Patterns: Reducing Technical Debt for Faster Development

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)