Type Safety Across Languages: A Comparative Analysis for Financial Systems
Comparing type systems in C++, Rust, OCaml, Python, and TypeScript, and how static typing prevents bugs in mission-critical financial applications.
Comparing type systems in C++, Rust, OCaml, Python, and TypeScript, and how static typing prevents bugs in mission-critical financial applications.
In financial systems, bugs aren't just inconvenient—they're catastrophic. A type error can result in incorrect trades worth millions, wrong account balances, or compliance violations. This article compares type systems across five languages commonly used in fintech and explores how static typing prevents entire categories of bugs.
Before diving into languages, let's understand what we're protecting against:
1// A real bug from a production trading system (simplified)
2double calculatePnL(double entryPrice, double exitPrice, int quantity) {
3 return (exitPrice - entryPrice) * quantity;
4}
5
6// Later in the codebase...
7double pnl = calculatePnL(quantity, exitPrice, entryPrice); // Arguments swapped!
8This compiled without warnings and ran in production for weeks before discovery. The cost? Incorrect P&L calculations affecting thousands of accounts.
Modern C++ provides powerful compile-time guarantees:
1#include <concepts>
2#include <string>
3#include <chrono>
4
5// Define concepts for financial types
6template<typename T>
7concept MonetaryValue = std::is_arithmetic_v<T> && requires(T a, T b) {
8 { a + b } -> std::convertible_to<T>;
9 { a - b } -> std::convertible_to<T>;
10 { a * b } -> std::convertible_to<T>;
11};
12
13template<typename T>
14concept Identifier = requires(T id) {
15 { id.toString() } -> std::convertible_to<std::string>;
16 { id.isValid() } -> std::convertible_to<bool>;
17};
18
19// Strong type wrappers
20template<typename T, typename Tag>
21class StrongType {
22 T value_;
23
24public:
25 explicit StrongType(T value) : value_(value) {}
26
27 T get() const { return value_; }
28
29 // Only allow operations between same types
30 StrongType operator+(const StrongType& other) const {
31 return StrongType(value_ + other.value_);
32 }
33
34 StrongType operator-(const StrongType& other) const {
35 return StrongType(value_ - other.value_);
36 }
37
38 // Prevent implicit conversions
39 template<typename U, typename OtherTag>
40 StrongType operator+(const StrongType<U, OtherTag>&) = delete;
41};
42
43// Domain types
44struct PriceTag {};
45struct QuantityTag {};
46struct AccountIdTag {};
47
48using Price = StrongType<double, PriceTag>;
49using Quantity = StrongType<int64_t, QuantityTag>;
50using AccountId = StrongType<std::string, AccountIdTag>;
51
52// Type-safe PnL calculation
53template<MonetaryValue T>
54double calculatePnL(Price entryPrice, Price exitPrice, Quantity quantity) {
55 return (exitPrice - entryPrice).get() * quantity.get();
56}
57
58// This won't compile - type mismatch!
59// auto pnl = calculatePnL(quantity, exitPrice, entryPrice); // ✗ Compile error
601class CurrencyPair {
2 std::string pair_;
3
4 static constexpr bool isValidCurrency(std::string_view curr) {
5 return curr == "USD" || curr == "EUR" || curr == "GBP" ||
6 curr == "JPY" || curr == "CHF";
7 }
8
9public:
10 constexpr CurrencyPair(std::string_view base, std::string_view quote)
11 : pair_(std::string(base) + "/" + std::string(quote)) {
12 if (!isValidCurrency(base) || !isValidCurrency(quote)) {
13 throw std::invalid_argument("Invalid currency");
14 }
15 }
16
17 constexpr std::string_view value() const { return pair_; }
18};
19
20// Validated at compile time!
21constexpr CurrencyPair EURUSD("EUR", "USD"); // ✓ OK
22// constexpr CurrencyPair INVALID("XXX", "USD"); // ✗ Compile error
231#include <variant>
2#include <optional>
3
4struct OrderPending {
5 std::chrono::system_clock::time_point submittedAt;
6};
7
8struct OrderAccepted {
9 std::string orderId;
10 std::chrono::system_clock::time_point acceptedAt;
11};
12
13struct OrderFilled {
14 std::string orderId;
15 Price fillPrice;
16 std::chrono::system_clock::time_point filledAt;
17};
18
19struct OrderRejected {
20 std::string reason;
21 std::chrono::system_clock::time_point rejectedAt;
22};
23
24using OrderStatus = std::variant<
25 OrderPending,
26 OrderAccepted,
27 OrderFilled,
28 OrderRejected
29>;
30
31struct Order {
32 std::string symbol;
33 Quantity quantity;
34 OrderStatus status;
35};
36
37// Type-safe status handling with visitor pattern
38std::optional<std::string> getOrderId(const Order& order) {
39 return std::visit([](auto&& status) -> std::optional<std::string> {
40 using T = std::decay_t<decltype(status)>;
41 if constexpr (std::is_same_v<T, OrderAccepted> ||
42 std::is_same_v<T, OrderFilled>) {
43 return status.orderId;
44 } else {
45 return std::nullopt;
46 }
47 }, order.status);
48}
49Rust's ownership system prevents entire classes of bugs at compile time:
1use std::marker::PhantomData;
2
3// Phantom types for zero-cost abstractions
4struct USD;
5struct EUR;
6struct GBP;
7
8#[derive(Debug, Clone, Copy)]
9struct Money<Currency> {
10 amount: i64, // Store as cents to avoid floating point
11 _currency: PhantomData<Currency>,
12}
13
14impl<C> Money<C> {
15 fn new(dollars: i64, cents: i64) -> Self {
16 Self {
17 amount: dollars * 100 + cents,
18 _currency: PhantomData,
19 }
20 }
21
22 fn amount_cents(&self) -> i64 {
23 self.amount
24 }
25
26 // Only allow adding same currency
27 fn add(self, other: Money<C>) -> Money<C> {
28 Money {
29 amount: self.amount + other.amount,
30 _currency: PhantomData,
31 }
32 }
33}
34
35// Prevent mixing currencies at compile time
36fn calculate_total(prices: Vec<Money<USD>>) -> Money<USD> {
37 prices.into_iter()
38 .fold(Money::new(0, 0), |acc, price| acc.add(price))
39}
40
41// This won't compile!
42// let usd = Money::<USD>::new(100, 0);
43// let eur = Money::<EUR>::new(100, 0);
44// let total = usd.add(eur); // ✗ Compile error: mismatched types
451// Wrapper types that prevent mixing
2#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
3struct AccountId(u64);
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
6struct OrderId(u64);
7
8#[derive(Debug, Clone, Copy)]
9struct Price(i64); // Price in cents
10
11#[derive(Debug, Clone, Copy)]
12struct Quantity(i64);
13
14impl AccountId {
15 fn new(id: u64) -> Result<Self, String> {
16 if id == 0 {
17 Err("Account ID cannot be zero".to_string())
18 } else {
19 Ok(AccountId(id))
20 }
21 }
22}
23
24impl Price {
25 fn new(dollars: i64, cents: i64) -> Self {
26 Price(dollars * 100 + cents)
27 }
28
29 fn as_float(&self) -> f64 {
30 self.0 as f64 / 100.0
31 }
32}
33
34// Type-safe function signature
35fn execute_order(
36 account: AccountId,
37 order: OrderId,
38 price: Price,
39 quantity: Quantity,
40) -> Result<(), String> {
41 // Can't accidentally swap parameters!
42 println!("Executing order {:?} for account {:?}", order, account);
43 Ok(())
44}
451struct Position {
2 symbol: String,
3 quantity: i64,
4 average_price: f64,
5}
6
7impl Position {
8 // Consuming self - can't use position after closing
9 fn close(self) -> f64 {
10 let pnl = self.quantity as f64 * self.average_price;
11 println!("Position closed");
12 pnl
13 }
14}
15
16fn demonstrate_ownership() {
17 let position = Position {
18 symbol: "AAPL".to_string(),
19 quantity: 100,
20 average_price: 150.0,
21 };
22
23 let pnl = position.close();
24
25 // This won't compile - position was moved!
26 // println!("{}", position.symbol); // ✗ Compile error
27}
281use std::fmt;
2
3#[derive(Debug)]
4enum TradingError {
5 InsufficientFunds { required: i64, available: i64 },
6 InvalidQuantity(i64),
7 MarketClosed,
8 InvalidSymbol(String),
9}
10
11impl fmt::Display for TradingError {
12 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
13 match self {
14 TradingError::InsufficientFunds { required, available } => {
15 write!(f, "Insufficient funds: need {}, have {}", required, available)
16 }
17 TradingError::InvalidQuantity(q) => {
18 write!(f, "Invalid quantity: {}", q)
19 }
20 TradingError::MarketClosed => write!(f, "Market is closed"),
21 TradingError::InvalidSymbol(s) => write!(f, "Invalid symbol: {}", s),
22 }
23 }
24}
25
26fn place_order(
27 account_id: AccountId,
28 symbol: &str,
29 quantity: Quantity,
30 price: Price,
31) -> Result<OrderId, TradingError> {
32 if quantity.0 <= 0 {
33 return Err(TradingError::InvalidQuantity(quantity.0));
34 }
35
36 // Business logic...
37 Ok(OrderId(12345))
38}
39
40// Compiler forces error handling
41fn execute_trade() {
42 match place_order(
43 AccountId::new(1).unwrap(),
44 "AAPL",
45 Quantity(100),
46 Price::new(150, 0),
47 ) {
48 Ok(order_id) => println!("Order placed: {:?}", order_id),
49 Err(e) => eprintln!("Trade failed: {}", e),
50 }
51}
52OCaml's type system excels at modeling complex domains:
1(* Phantom types for currency *)
2type usd
3type eur
4type gbp
5
6type 'currency money = {
7 cents : int64;
8}
9
10let usd cents = { cents }
11let eur cents = { cents }
12
13(* Only allow same-currency operations *)
14let add_money : 'c money -> 'c money -> 'c money =
15 fun m1 m2 -> { cents = Int64.add m1.cents m2.cents }
16
17(* This type-checks *)
18let total_usd = add_money (usd 100L) (usd 200L)
19
20(* This doesn't compile! *)
21(* let invalid = add_money (usd 100L) (eur 200L) *)
22
23(* Variant types for order status *)
24type order_status =
25 | Pending of { submitted_at : float }
26 | Accepted of { order_id : string; accepted_at : float }
27 | Filled of { order_id : string; fill_price : int64; filled_at : float }
28 | Rejected of { reason : string; rejected_at : float }
29 | Cancelled of { order_id : string; cancelled_at : float }
30
31type order = {
32 account_id : string;
33 symbol : string;
34 quantity : int64;
35 status : order_status;
36}
37
38(* Exhaustive pattern matching - compiler ensures all cases handled *)
39let get_order_id order =
40 match order.status with
41 | Pending _ | Rejected _ -> None
42 | Accepted { order_id; _ }
43 | Filled { order_id; _ }
44 | Cancelled { order_id; _ } -> Some order_id
45
46(* GADTs for even more type safety *)
47type _ state =
48 | Pending : pending state
49 | Accepted : accepted state
50 | Filled : filled state
51
52and pending = { submitted_at : float }
53and accepted = { order_id : string; accepted_at : float }
54and filled = { order_id : string; fill_price : int64; filled_at : float }
55
56type 'a order_state = {
57 account_id : string;
58 symbol : string;
59 quantity : int64;
60 state : 'a;
61}
62
63(* Only accepted or filled orders have order_id *)
64let get_id : type a. a state -> a -> string option =
65 fun state data ->
66 match state with
67 | Pending -> None
68 | Accepted -> Some data.order_id
69 | Filled -> Some data.order_id
701(* Abstract signature for monetary types *)
2module type MONEY = sig
3 type t
4 val zero : t
5 val of_cents : int64 -> t
6 val to_cents : t -> int64
7 val add : t -> t -> t
8 val sub : t -> t -> t
9 val mul : t -> int64 -> t
10 val compare : t -> t -> int
11end
12
13(* Implementation ensures invariants *)
14module Money : MONEY = struct
15 type t = int64 (* Always stored as cents *)
16
17 let zero = 0L
18
19 let of_cents cents =
20 if cents < 0L then
21 invalid_arg "Money cannot be negative"
22 else cents
23
24 let to_cents t = t
25
26 let add = Int64.add
27 let sub a b =
28 let result = Int64.sub a b in
29 if result < 0L then
30 invalid_arg "Result cannot be negative"
31 else result
32
33 let mul t factor = Int64.mul t factor
34
35 let compare = Int64.compare
36end
37
38(* Functor for currency-specific operations *)
39module type CURRENCY = sig
40 val code : string
41 val symbol : string
42end
43
44module MakeMoneyOps (C : CURRENCY) = struct
45 include Money
46
47 let currency_code = C.code
48
49 let to_string t =
50 let cents = to_cents t in
51 let dollars = Int64.div cents 100L in
52 let remainder = Int64.rem cents 100L in
53 Printf.sprintf "%s%Ld.%02Ld" C.symbol dollars remainder
54end
55
56module USD_Currency = struct
57 let code = "USD"
58 let symbol = "$"
59end
60
61module USD = MakeMoneyOps(USD_Currency)
62
63let price = USD.of_cents 15050L
64let () = print_endline (USD.to_string price) (* Prints: $150.50 *)
65Python 3.5+ supports gradual typing:
1from typing import NewType, Protocol, Literal, TypeVar, Generic
2from decimal import Decimal
3from datetime import datetime
4from dataclasses import dataclass
5
6# NewType for distinct types
7AccountId = NewType('AccountId', str)
8OrderId = NewType('OrderId', str)
9Symbol = NewType('Symbol', str)
10
11def validate_account_id(value: str) -> AccountId:
12 if not value.startswith('ACC'):
13 raise ValueError(f"Invalid account ID: {value}")
14 return AccountId(value)
15
16# Generic types for currency
17T = TypeVar('T', bound='Currency')
18
19class Currency(Protocol):
20 code: str
21 symbol: str
22
23@dataclass
24class USD:
25 code: str = 'USD'
26 symbol: str = '$'
27
28@dataclass
29class EUR:
30 code: str = 'EUR'
31 symbol: str = '€'
32
33@dataclass
34class Money(Generic[T]):
35 """Type-safe money representation"""
36 cents: int
37 currency: T
38
39 def __init__(self, amount: Decimal, currency: T):
40 if amount < 0:
41 raise ValueError("Amount cannot be negative")
42 self.cents = int(amount * 100)
43 self.currency = currency
44
45 def __add__(self: 'Money[T]', other: 'Money[T]') -> 'Money[T]':
46 if self.currency.code != other.currency.code:
47 raise ValueError("Currency mismatch")
48 return Money(
49 Decimal(self.cents + other.cents) / 100,
50 self.currency
51 )
52
53 @property
54 def amount(self) -> Decimal:
55 return Decimal(self.cents) / 100
56
57# Literal types for constrained values
58OrderType = Literal['market', 'limit']
59Side = Literal['buy', 'sell']
60
61@dataclass
62class MarketOrder:
63 order_type: Literal['market'] = 'market'
64 account_id: AccountId
65 symbol: Symbol
66 quantity: int
67 side: Side
68
69@dataclass
70class LimitOrder:
71 order_type: Literal['limit'] = 'limit'
72 account_id: AccountId
73 symbol: Symbol
74 quantity: int
75 side: Side
76 limit_price: Money[USD]
77
78Order = MarketOrder | LimitOrder # Union type
79
80def process_order(order: Order) -> OrderId:
81 """Type-safe order processing"""
82 match order:
83 case MarketOrder(account_id=acc, symbol=sym, quantity=qty):
84 print(f"Processing market order: {qty} {sym}")
85 case LimitOrder(account_id=acc, limit_price=price):
86 print(f"Processing limit order at {price.amount}")
87
88 return OrderId("ORD123456")
89
90# Runtime type checking with Pydantic
91from pydantic import BaseModel, Field, validator
92
93class OrderRequest(BaseModel):
94 account_id: str = Field(..., regex=r'^ACC\d{10}$')
95 symbol: str = Field(..., regex=r'^[A-Z]{1,5}$')
96 quantity: int = Field(..., gt=0)
97 side: Literal['buy', 'sell']
98 order_type: Literal['market', 'limit']
99 limit_price: Decimal | None = None
100
101 @validator('limit_price')
102 def check_limit_price(cls, v, values):
103 if values.get('order_type') == 'limit' and v is None:
104 raise ValueError('Limit orders must have a limit price')
105 return v
106
107 class Config:
108 frozen = True # Immutable
109
110# Usage with full type safety
111try:
112 order = OrderRequest(
113 account_id="ACC0000000001",
114 symbol="AAPL",
115 quantity=100,
116 side="buy",
117 order_type="limit",
118 limit_price=Decimal("150.50")
119 )
120except ValueError as e:
121 print(f"Validation error: {e}")
122See our article "TypeScript Type Safety in Financial Applications: Beyond the Basics" for comprehensive TypeScript patterns.
| Feature | C++ | Rust | OCaml | Python | TypeScript |
|---|---|---|---|---|---|
| Static Typing | ✓ | ✓ | ✓ | Optional | Optional |
| Type Inference | ✓ | ✓✓ | ✓✓ | ✓ | ✓✓ |
| Null Safety | - | ✓✓ | ✓✓ | - | ✓ |
| Immutability | ✓ | ✓✓ | ✓✓ | - | ✓ |
| Pattern Matching | ✓ | ✓✓ | ✓✓ | ✓ | ✓ |
| Memory Safety | - | ✓✓ | ✓ | ✓ | ✓ |
| Compile-time Guarantees | ✓✓ | ✓✓ | ✓✓ | - | ✓ |
C++:
Rust:
OCaml:
Python:
TypeScript:
Across our fintech projects, static typing has delivered:
Static typing is not just a nice-to-have—it's essential for building reliable financial systems. The type system should be your first line of defense against bugs.
Choose the language whose type system best matches your domain:
The upfront investment in proper typing pays enormous dividends in reduced bugs, faster development, and increased confidence in your system's correctness.
Technical Writer
NordVarg Engineering Team is a software engineer at NordVarg specializing in high-performance financial systems and type-safe programming.
Get weekly insights on building high-performance financial systems, latest industry trends, and expert tips delivered straight to your inbox.