sunghyun-k/swift-typescript-bridge
Bring TypeScript-style union types to Swift with type safety and automatic JSON encoding/decoding.
Why This Library?
If you've worked with TypeScript and Swift together, you know the pain of translating TypeScript's flexible union types into Swift. This library solves that.
TypeScript
type Status = "pending" | "approved" | "rejected";
type StatusCode = 200 | 404 | 500;
type Response = SuccessResponse | ErrorResponse;Swift (without this library)
// Verbose enum definitions, manual Codable implementation, separate rawValue handling...
enum Status: String, Codable {
case pending = "pending"
case approved = "approved"
case rejected = "rejected"
}Swift (with this library)
@Union("pending", "approved", "rejected") enum Status {}
@Union(200, 404, 500) enum StatusCode {}
@Union(SuccessResponse.self, ErrorResponse.self) enum Response {}
// Codable conformance included ✨Installation
Swift Package Manager
dependencies: [
.package(url: "https://github.com/sunghyun-k/swift-typescript-bridge.git", from: "0.3.0")
]Or in Xcode: File → Add Package Dependencies
Core Features
1. Literal Unions
Map TypeScript literal unions directly to Swift.
// TypeScript
interface Event {
type: "click" | "hover" | "focus";
timestamp: number;
}// Swift
@Union("click", "hover", "focus") enum EventType {}
struct Event: Codable {
let type: EventType
let timestamp: Double
}
let event = Event(type: .click, timestamp: Date().timeIntervalSince1970)Works with strings, integers, doubles, and booleans:
@Union("auto", 100, true, 2.5) enum ConfigValue {}2. Type Unions
Combine different Swift types into one union.
// TypeScript
type Entity = User | Organization;// Swift
@Union(User.self, Organization.self) enum Entity {}
let user = User(name: "Alice")
let entity = Entity.user(user)3. Discriminated Unions
The killer feature: efficient JSON decoding with discriminator fields.
// TypeScript - Discriminated union pattern
interface SuccessResponse {
status: "success";
data: { id: string; name: string };
}
interface ErrorResponse {
status: "error";
error: { code: string; message: string };
}
type ApiResponse = SuccessResponse | ErrorResponse;// Swift - Same pattern, same efficiency
@UnionDiscriminator("status")
struct SuccessResponse: Codable {
@Union("success") enum Status {}
let status: Status
let data: SuccessData
struct SuccessData: Codable {
let id: String
let name: String
}
}
@UnionDiscriminator("status")
struct ErrorResponse: Codable {
@Union("error") enum Status {}
let status: Status
let error: ErrorData
struct ErrorData: Codable {
let code: String
let message: String
}
}
@Union(SuccessResponse.self, ErrorResponse.self) enum ApiResponse {}
// JSON decoding - discriminator field checked first for fast, accurate type detection
let response = try JSONDecoder().decode(ApiResponse.self, from: jsonData)Why discriminated unions matter: Without @UnionDiscriminator, the decoder tries each type sequentially until one succeeds—slow and error-prone. With it, the decoder checks the discriminator field first and decodes the correct type immediately.
4. Type Extension (Extends)
Bring TypeScript's interface B extends A pattern to Swift structs. Flat JSON encoding/decoding and property forwarding are generated automatically.
// TypeScript
interface BaseEvent {
timestamp: number;
}
interface ClickEvent extends BaseEvent {
x: number;
y: number;
}// Swift
struct BaseEvent: Codable {
var timestamp: Double
}
@Extends(BaseEvent.self)
struct ClickEvent {
var x: Int
var y: Int
}
let c = ClickEvent(BaseEvent(timestamp: 0), x: 10, y: 20)
c.timestamp // forwarded from BaseEvent
c.x // 10
// JSON: {"timestamp":0,"x":10,"y":20} — flat!Narrowing parent properties: A child can redeclare a parent property to narrow its type (e.g., parent's String → child's literal union). The child's stored property shadows the forwarded parent property, and the narrower type is enforced on decode.
struct Event: Codable {
var kind: String // parent: any string
var name: String
}
@Extends(Event.self)
struct ClickEvent {
@Union("click") enum Kind {}
var kind: Kind // child: narrowed to "click"
}Limitations:
- Property overrides with incompatible JSON representations (e.g., parent
Int, childString) will fail to decode. - MVP supports a single parent only.
Real-World Example
Parse web analytics events from your TypeScript frontend:
// TypeScript Frontend
interface PageViewEvent {
event: "page_view";
page: string;
}
interface UserActionEvent {
event: "click" | "scroll";
element: string;
}
type WebEvent = PageViewEvent | UserActionEvent;// Swift Backend
@UnionDiscriminator("event")
struct PageViewEvent: Codable {
@Union("page_view") enum EventType {}
let event: EventType
let page: String
}
@UnionDiscriminator("event")
struct UserActionEvent: Codable {
@Union("click", "scroll") enum EventType {}
let event: EventType
let element: String
}
@Union(PageViewEvent.self, UserActionEvent.self) enum WebEvent {}
// Parse incoming analytics
let analyticsEvent = try JSONDecoder().decode(WebEvent.self, from: jsonData)How It Works
Built on Swift macros—all code generation happens at compile time with zero runtime overhead. Expand macros in Xcode to see exactly what's generated.
Requirements
- Swift 6.2+
- Platforms: iOS 13+, macOS 10.15+, tvOS 13+, watchOS 6+, Linux
License
MIT License - see LICENSE
Package Metadata
Repository: sunghyun-k/swift-typescript-bridge
Default branch: main
README: README.md