Skip to main content
Examples Verified (100%)

Optional & Rest Parameters

Ruby functions often need flexibility in their parameter lists. T-Ruby supports both optional parameters (with default values) and rest parameters (variable-length argument lists) while maintaining full type safety.

Optional Parameters with Default Values

Optional parameters have default values that are used when the argument is not provided:

optional.trb
def greet(name: String, greeting: String = "Hello"): String
"#{greeting}, #{name}!"
end

def create_user(name: String, role: String = "user", active: Boolean = true): User
User.new(name: name, role: role, active: active)
end

# Calling with different numbers of arguments
puts greet("Alice") # "Hello, Alice!"
puts greet("Bob", "Hi") # "Hi, Bob!"

user1 = create_user("Alice") # Uses defaults: role="user", active=true
user2 = create_user("Bob", "admin") # Uses default: active=true
user3 = create_user("Charlie", "moderator", false) # No defaults used

The type annotation applies to the parameter whether it's provided or uses the default value.

Optional Parameters with Nilable Types

Sometimes you want to distinguish between "not provided" and "explicitly nil". Use a nilable type:

nilable_optional.trb
def format_title(text: String, prefix: String? = nil): String
if prefix
"#{prefix}: #{text}"
else
text
end
end

def send_email(to: String, subject: String, cc: String? = nil): void
email = Email.new(to: to, subject: subject)
email.cc = cc if cc
email.send
end

# Using nilable optional parameters
title1 = format_title("Introduction") # "Introduction"
title2 = format_title("Chapter 1", "Book") # "Book: Chapter 1"
title3 = format_title("Epilogue", nil) # "Epilogue"

send_email("alice@example.com", "Hello")
send_email("bob@example.com", "Meeting", "team@example.com")

Rest Parameters

Rest parameters collect multiple arguments into an array. Type the array's element type:

rest.trb
def sum(*numbers: Integer[]): Integer
numbers.reduce(0, :+)
end

def concat_strings(*strings: String[]): String
strings.join(" ")
end

def log_messages(level: String, *messages: String[]): void
messages.each do |msg|
puts "[#{level}] #{msg}"
end
end

# Calling with variable arguments
puts sum(1, 2, 3, 4, 5) # 15
puts sum(10) # 10
puts sum() # 0

result = concat_strings("Hello", "world", "from", "T-Ruby")
# "Hello world from T-Ruby"

log_messages("INFO", "App started", "Database connected", "Ready")
# [INFO] App started
# [INFO] Database connected
# [INFO] Ready

The type annotation *numbers: Integer[] means "zero or more Integer arguments collected into an array".

Combining Optional and Rest Parameters

You can combine optional and rest parameters, but rest parameters must come after optional ones:

combined.trb
def create_team(
name: String,
leader: String,
active: Boolean = true,
*members: String[]
): Team
Team.new(
name: name,
leader: leader,
active: active,
members: members
)
end

# Various ways to call
team1 = create_team("Alpha", "Alice")
# name="Alpha", leader="Alice", active=true, members=[]

team2 = create_team("Beta", "Bob", false)
# name="Beta", leader="Bob", active=false, members=[]

team3 = create_team("Gamma", "Charlie", true, "Dave", "Eve", "Frank")
# name="Gamma", leader="Charlie", active=true, members=["Dave", "Eve", "Frank"]

Keyword Arguments

In T-Ruby, keyword arguments are defined using the { } syntax (without variable name). Unlike positional arguments, they are passed by name when calling.

Positional vs Keyword Arguments
DefinitionCall Style
(x: String, y: Integer)foo("hi", 10) - positional
({ x: String, y: Integer })foo(x: "hi", y: 10) - keyword
(config: { x: String })foo(config: { x: "hi" }) - Hash literal

Key Rule: Variable name presence determines the meaning:

  • { ... } (no variable) → keyword arguments (destructured)
  • name: { ... } (with variable) → Hash literal

Inline Type Style

Define types directly inside { }. Default values use = value syntax:

keyword_inline.trb
def create_post({
title: String,
content: String,
published: Boolean = false,
tags: String[] = []
}): Post
Post.new(
title: title,
content: content,
published: published,
tags: tags
)
end

# Calling with keyword arguments (order doesn't matter)
post1 = create_post(
title: "My First Post",
content: "Hello world"
)

post2 = create_post(
content: "Another post",
title: "Second Post",
published: true,
tags: ["ruby", "programming"]
)

Interface Reference Style

Define a separate interface and reference it. Default values use Ruby-style : value (no equals sign):

keyword_interface.trb
interface PostOptions
title: String
content: String
published?: Boolean # ? marks optional
tags?: String[]
end

def create_post({ title:, content:, published: false, tags: [] }: PostOptions): Post
Post.new(
title: title,
content: content,
published: published,
tags: tags
)
end

# Call style is the same
post = create_post(title: "Hello", content: "World")
Choosing Between Inline and Interface
AspectInline TypeInterface Reference
Type definitionInside { }Separate interface
Default value syntax= value: value (no equals)
Optional markingImplied by default value? suffix
ReusabilitySingle methodShared across methods

Recommendation: Use inline for single-method use, interface for reuse across multiple methods

Keyword Rest Parameters

Use double splat ** to collect keyword arguments into a hash:

keyword_rest.trb
def build_query(table: String, **conditions: Hash<Symbol, String | Integer>): String
where_clause = conditions.map { |k, v| "#{k} = #{v}" }.join(" AND ")
"SELECT * FROM #{table} WHERE #{where_clause}"
end

def create_config(env: String, **options: Hash<Symbol, String | Integer | Boolean>): Config
Config.new(environment: env, options: options)
end

# Using keyword rest parameters
query1 = build_query("users", id: 123, active: 1)
# "SELECT * FROM users WHERE id = 123 AND active = 1"

query2 = build_query("posts", author_id: 5, published: 1, category: "tech")
# "SELECT * FROM posts WHERE author_id = 5 AND published = 1 AND category = tech"

config = create_config(
"production",
debug: false,
timeout: 30,
host: "example.com"
)

The type annotation **conditions: Hash<Symbol, String | Integer> means "zero or more keyword arguments with String or Integer values collected into a hash".

Required Keyword Arguments

Keyword arguments without default values are required:

required_kwargs.trb
def register_user({
email: String,
password: String,
name: String = "Anonymous",
age: Integer
}): User
# email, password, and age are required
# name is optional with default
User.new(email: email, password: password, name: name, age: age)
end

# Must provide email, password, and age
user = register_user(
email: "alice@example.com",
password: "secret123",
age: 25
)

# Can optionally override name
user2 = register_user(
email: "bob@example.com",
password: "secret456",
name: "Bob",
age: 30
)

Mixing Positional, Optional, and Rest Parameters

You can combine all parameter types, but they must follow this order:

  1. Required positional parameters
  2. Optional positional parameters
  3. Rest parameter (*args)
  4. Keyword arguments ({ ... })
  5. Keyword rest parameter (**kwargs)
all_types.trb
def complex_function(
required_pos: String, # 1. Required positional
optional_pos: Integer = 0, # 2. Optional positional
*rest_args: String[], # 3. Rest parameter
{
required_kw: Boolean, # 4. Required keyword
optional_kw: String = "default" # 5. Optional keyword
},
**rest_kwargs: Hash<Symbol, String | Integer> # 6. Keyword rest
): Hash<String, String | Integer | Boolean>
{
"required_pos" => required_pos,
"optional_pos" => optional_pos,
"rest_args" => rest_args.join(","),
"required_kw" => required_kw,
"optional_kw" => optional_kw,
"rest_kwargs" => rest_kwargs
}
end

# Example call
result = complex_function(
"hello", # required_pos
42, # optional_pos
"a", "b", "c", # rest_args
required_kw: true, # required_kw
optional_kw: "custom", # optional_kw
extra1: "value1", # rest_kwargs
extra2: 123 # rest_kwargs
)

Practical Example: HTTP Request Builder

Here's a real-world example showing different parameter types:

http_builder.trb
class HTTPRequestBuilder
# Required parameters only
def get(url: String): Response
make_request("GET", url, nil, {})
end

# Required + optional parameters
def post(url: String, body: String, content_type: String = "application/json"): Response
headers = { "Content-Type" => content_type }
make_request("POST", url, body, headers)
end

# Required + rest parameters
def delete(*urls: String[]): Response[]
urls.map { |url| make_request("DELETE", url, nil, {}) }
end

# Keyword arguments (inline type)
def request({
method: String,
url: String,
body: String? = nil,
timeout: Integer = 30,
retry_count: Integer = 3
}): Response
make_request(method, url, body, {}, timeout, retry_count)
end

# Keyword rest parameters
def custom_request(
method: String,
url: String,
**headers: Hash<Symbol, String>
): Response
make_request(method, url, nil, headers)
end

private

def make_request(
method: String,
url: String,
body: String?,
headers: Hash<String, String>,
timeout: Integer = 30,
retry_count: Integer = 3
): Response
# Implementation details...
Response.new
end
end

# Using the builder
builder = HTTPRequestBuilder.new

# Simple GET
response1 = builder.get("https://api.example.com/users")

# POST with custom content type
response2 = builder.post(
"https://api.example.com/users",
'{"name": "Alice"}',
"application/json"
)

# DELETE multiple resources
responses = builder.delete(
"https://api.example.com/users/1",
"https://api.example.com/users/2",
"https://api.example.com/users/3"
)

# Request with custom options
response3 = builder.request(
method: "PATCH",
url: "https://api.example.com/users/1",
body: '{"age": 31}',
timeout: 60,
retry_count: 5
)

# Request with custom headers
response4 = builder.custom_request(
method: "GET",
url: "https://api.example.com/protected",
Authorization: "Bearer token123",
Accept: "application/json",
User_Agent: "MyApp/1.0"
)

Practical Example: Logger

Another example showing flexible parameter handling:

logger.trb
class Logger
# Simple message with optional level
def log(message: String, level: String = "INFO"): void
puts "[#{level}] #{message}"
end

# Multiple messages with rest parameter
def log_many(level: String, *messages: String[]): void
messages.each { |msg| log(msg, level) }
end

# Structured logging with keyword rest
def log_structured(message: String, **metadata: Hash<Symbol, String | Integer | Boolean>): void
meta_str = metadata.map { |k, v| "#{k}=#{v}" }.join(" ")
puts "[INFO] #{message} | #{meta_str}"
end

# Flexible debug logging
def debug(*messages: String[], **context: Hash<Symbol, String | Integer>): void
messages.each do |msg|
ctx_str = context.empty? ? "" : " (#{context.map { |k, v| "#{k}=#{v}" }.join(", ")})"
puts "[DEBUG] #{msg}#{ctx_str}"
end
end
end

# Using the logger
logger = Logger.new

# Simple logging
logger.log("Application started")
logger.log("Warning: Low memory", "WARN")

# Multiple messages
logger.log_many("ERROR", "Database connection failed", "Retrying...", "Giving up")

# Structured logging
logger.log_structured(
"User logged in",
user_id: 123,
ip: "192.168.1.1",
success: true
)

# Debug with context
logger.debug(
"Processing request",
"Validating data",
"Saving to database",
request_id: 789,
user_id: 123
)

Best Practices

  1. Use default values for truly optional behavior: Only add default values when it makes sense for the parameter to be optional.

  2. Order parameters logically: Put required parameters first, then optional ones, then rest parameters.

  3. Prefer keyword arguments for clarity: When you have multiple optional parameters, keyword arguments make calls more readable.

  4. Use rest parameters for collections: When you expect a variable number of similar items, rest parameters are cleaner than array parameters.

  5. Type rest parameters appropriately: *args: String[] is better than *args: (String | Integer)[] if you only expect strings.

  6. Document complex signatures: When combining many parameter types, add comments explaining the usage.

Common Patterns

Builder Methods with Defaults (Keyword Arguments)

builder_pattern.trb
def build_email({
to: String,
subject: String,
from: String = "noreply@example.com",
reply_to: String? = nil,
cc: String[] = [],
bcc: String[] = []
}): Email
Email.new(to, subject, from, reply_to, cc, bcc)
end

# Call with keyword arguments
email = build_email(to: "alice@example.com", subject: "Hello")

Variadic Factory Functions (Rest + Keyword Arguments)

factory.trb
def create_users(*names: String[], { role: String = "user" }): User[]
names.map { |name| User.new(name: name, role: role) }
end

users = create_users("Alice", "Bob", "Charlie", role: "admin")

Configuration Merging

config.trb
def merge_config(base: Hash<String, String>, **overrides: Hash<Symbol, String>): Hash<String, String>
base.merge(overrides)
end

config = merge_config(
{ "host" => "localhost", "port" => "3000" },
port: "8080",
ssl: "true"
)

Summary

Optional and rest parameters give your functions flexibility while maintaining type safety:

SyntaxDescriptionCall Example
(x: Type)Positional argumentfoo("hi")
(x: Type = default)Optional positionalfoo() or foo("hi")
(*args: Type[])Rest parameterfoo("a", "b", "c")
({ x: Type })Keyword argumentfoo(x: "hi")
(config: { x: Type })Hash literalfoo(config: { x: "hi" })
(**kwargs: Hash<Symbol, Type>)Keyword restfoo(a: 1, b: 2)

Key Points:

  • Positional arguments (x: Type): passed by order
  • Keyword arguments ({ x: Type }): passed by name (no variable = destructured)
  • Hash literal (config: { x: Type }): with variable name = Hash
  • Keyword rest (**kwargs: Hash<Symbol, Type>): collects arbitrary keyword arguments into a hash
  • T-Ruby ensures type safety for all parameter variations

Master these patterns to write flexible, type-safe APIs that are pleasant to use.