Variables and Types
Utah provides a strong type system inspired by TypeScript, allowing you to write more reliable shell scripts with compile-time type checking.
Variable Declarations
let
- Mutable Variables
Use let
to declare mutable variables:
let userName: string = "Alice";
let count: number = 42;
let isActive: boolean = true;
// Variables can be reassigned
userName = "Bob";
count = count + 1;
isActive = false;
Generated bash:
userName="Alice"
count="42"
isActive="true"
userName="Bob"
count=$((count + 1))
isActive="false"
const
- Immutable Constants
Use const
for values that won't change:
const APP_NAME: string = "MyApplication";
const MAX_RETRIES: number = 3;
const DEBUG_MODE: boolean = false;
// Error: Cannot reassign const variables
// APP_NAME = "OtherApp"; // This would cause an error
Generated bash:
readonly APP_NAME="MyApplication"
readonly MAX_RETRIES="3"
readonly DEBUG_MODE="false"
Type Annotations
Utah supports several built-in types:
Basic Types
string
Text and character data:
let message: string = "Hello, Utah!";
let templateLiteral: string = "Dynamic: ${message}";
number
Numeric values (integers and decimals):
let integer: number = 42;
let decimal: number = 3.14;
let negative: number = -10;
let calculated: number = 5 + 3;
boolean
True/false values:
let isEnabled: boolean = true;
let isDisabled: boolean = false;
let computed: boolean = count > 0;
Array Types
string[]
Arrays of strings:
let names: string[] = ["Alice", "Bob", "Charlie"];
let fruits: string[] = []; // Empty array
let colors: string[] = ["red", "green", "blue"];
// Access elements
let firstName: string = names[0]; // "Alice"
let lastColor: string = colors[2]; // "blue"
number[]
Arrays of numbers:
let scores: number[] = [85, 92, 78, 90];
let fibonacci: number[] = [1, 1, 2, 3, 5, 8];
let measurements: number[] = [1.5, 2.7, 3.8];
// Array operations
let firstScore: number = scores[0];
let arrayLength: number = scores.length;
Object Type
object
For JSON/YAML objects:
// Parse JSON into object
let configJson: string = "{\"host\": \"localhost\", \"port\": 8080}";
let config: object = json.parse(configJson);
// Parse YAML into object
let yamlConfig: string = "host: localhost\nport: 8080";
let yamlObj: object = yaml.parse(yamlConfig);
// Access object properties
let host: string = json.get(config, ".host");
let port: number = json.get(config, ".port");
Type Inference
While type annotations are recommended, Utah can infer types in some cases:
// Explicit typing (preferred)
let name: string = "Utah";
let version: number = 1.0;
// Type inference (less clear)
let name = "Utah"; // Inferred as string
let version = 1.0; // Inferred as number
String Interpolation
Use template literals with ${}
for string interpolation:
let userName: string = "Alice";
let score: number = 95;
let isWinner: boolean = score > 90;
let message: string = "Hello ${userName}!";
let result: string = "Score: ${score}, Winner: ${isWinner}";
let complex: string = "User ${userName} scored ${score} points and ${isWinner ? \"won\" : \"lost\"}";
Generated bash:
userName="Alice"
score="95"
isWinner="true"
message="Hello ${userName}!"
result="Score: ${score}, Winner: ${isWinner}"
if [ "${isWinner}" = "true" ]; then
complex="User ${userName} scored ${score} points and won"
else
complex="User ${userName} scored ${score} points and lost"
fi
Variable Scope
Global Variables
Variables declared at the script level are global:
let globalVar: string = "Available everywhere";
const GLOBAL_CONSTANT: number = 42;
function useGlobals(): void {
console.log(globalVar); // Accessible
console.log(GLOBAL_CONSTANT); // Accessible
}
Function Parameters
Function parameters are local to the function:
function greet(name: string, age: number): void {
// 'name' and 'age' are only available within this function
let greeting: string = "Hello ${name}, you are ${age} years old";
console.log(greeting);
}
greet("Alice", 30);
// console.log(name); // Error: 'name' not available here
Generated bash:
greet() {
local name="$1"
local age="$2"
local greeting="Hello ${name}, you are ${age} years old"
echo "${greeting}"
}
Arrays in Detail
Array Declaration and Initialization
// Different ways to declare arrays
let emptyStrings: string[] = [];
let emptyNumbers: number[] = [];
let languages: string[] = ["JavaScript", "TypeScript", "Utah"];
let versions: number[] = [1, 2, 3, 4, 5];
// Mixed initialization
let mixed: string[] = ["first"];
mixed[1] = "second";
mixed[2] = "third";
Array Access and Properties
let items: string[] = ["apple", "banana", "cherry"];
// Access by index
let first: string = items[0]; // "apple"
let second: string = items[1]; // "banana"
let last: string = items[2]; // "cherry"
// Array properties
let length: number = array.length(items); // 3
let isEmpty: boolean = array.isEmpty(items); // false
Array Methods
let numbers: number[] = [3, 1, 4, 1, 5];
// Check if array contains item
let hasThree: boolean = array.contains(numbers, 3); // true
let hasTen: boolean = array.contains(numbers, 10); // false
// Reverse array
let reversed: number[] = array.reverse(numbers); // [5, 1, 4, 1, 3]
// Join array elements
let joined: string = array.join(["a", "b", "c"], "-"); // "a-b-c"
Array Iteration
let fruits: string[] = ["apple", "banana", "orange"];
// For-in loop (recommended)
for (let fruit: string in fruits) {
console.log("Fruit: ${fruit}");
}
// Traditional for loop
for (let i: number = 0; i < fruits.length; i++) {
let fruit: string = fruits[i];
console.log("Index ${i}: ${fruit}");
}
Working with Objects
Objects in Utah are primarily used for JSON and YAML data:
JSON Objects
// Parse JSON
let userJson: string = "{\"name\": \"Alice\", \"age\": 30, \"admin\": true}";
let user: object = json.parse(userJson);
// Access properties
let name: string = json.get(user, ".name");
let age: number = json.get(user, ".age");
let isAdmin: boolean = json.get(user, ".admin");
// Modify objects
user = json.set(user, ".name", "Bob");
user = json.set(user, ".lastLogin", "2023-12-01");
// Convert back to JSON
let updatedJson: string = json.stringify(user);
YAML Objects
// Parse YAML
let configYaml: string = `
database:
host: localhost
port: 5432
features:
- logging
- monitoring
`;
let config: object = yaml.parse(configYaml);
// Access nested properties
let dbHost: string = yaml.get(config, ".database.host");
let dbPort: number = yaml.get(config, ".database.port");
let firstFeature: string = yaml.get(config, ".features[0]");
// Check property existence
let hasDatabase: boolean = yaml.has(config, ".database");
let hasAuth: boolean = yaml.has(config, ".auth");
Type Conversion
Utah handles type conversions automatically in many cases:
String to Number
let input: string = "42";
let num: number = parseInt(input); // Manual parsing if needed
let calculated: number = 10 + 5; // Direct numeric operation
Boolean Conversion
let flag: string = "true";
let isTrue: boolean = flag == "true"; // String comparison to boolean
let count: number = 5;
let hasItems: boolean = count > 0; // Numeric comparison to boolean
Template Literal Conversion
let count: number = 42;
let active: boolean = true;
// Automatic conversion in template literals
let message: string = "Count: ${count}, Active: ${active}";
// Result: "Count: 42, Active: true"
Environment Variables
Access environment variables with proper typing:
// Get environment variable with default
let dbHost: string = env.get("DB_HOST", "localhost");
let dbPort: number = parseInt(env.get("DB_PORT", "5432"));
let debugMode: boolean = env.get("DEBUG", "false") == "true";
// Use environment variables
console.log("Connecting to ${dbHost}:${dbPort}");
if (debugMode) {
console.log("Debug mode enabled");
}
Best Practices
1. Always Use Type Annotations
// Good - explicit and clear
let userName: string = "Alice";
let userCount: number = 42;
let isActive: boolean = true;
// Avoid - unclear types
let userName = "Alice";
let userCount = 42;
let isActive = true;
2. Use Descriptive Variable Names
// Good - self-documenting
let databaseConnectionString: string = "localhost:5432";
let maxRetryAttempts: number = 3;
let isUserAuthenticated: boolean = false;
// Avoid - unclear meaning
let db: string = "localhost:5432";
let max: number = 3;
let auth: boolean = false;
3. Use Constants for Fixed Values
// Good - prevents accidental changes
const API_ENDPOINT: string = "https://api.example.com";
const TIMEOUT_SECONDS: number = 30;
const ENABLE_LOGGING: boolean = true;
// Less safe - might be changed accidentally
let apiEndpoint: string = "https://api.example.com";
4. Initialize Arrays Explicitly
// Good - clear intent
let usernames: string[] = [];
let scores: number[] = [0, 0, 0];
// Clear when adding items
usernames[0] = "alice";
usernames[1] = "bob";
5. Validate Data Before Use
// Good - safe data handling
let configData: string = fs.readFile("config.json");
if (json.isValid(configData)) {
let config: object = json.parse(configData);
let host: string = json.get(config, ".database.host");
} else {
console.log("Invalid configuration file");
exit(1);
}
Common Patterns
Configuration Loading
const CONFIG_FILE: string = "app.json";
const DEFAULT_PORT: number = 8080;
let port: number = DEFAULT_PORT;
let host: string = "localhost";
if (fs.exists(CONFIG_FILE)) {
let configContent: string = fs.readFile(CONFIG_FILE);
if (json.isValid(configContent)) {
let config: object = json.parse(configContent);
if (json.has(config, ".port")) {
port = json.get(config, ".port");
}
if (json.has(config, ".host")) {
host = json.get(config, ".host");
}
}
}
console.log("Server will start on ${host}:${port}");
Data Processing Pipeline
let inputFiles: string[] = ["data1.json", "data2.json", "data3.json"];
let processedCount: number = 0;
let errors: string[] = [];
for (let file: string in inputFiles) {
try {
if (fs.exists(file)) {
let content: string = fs.readFile(file);
if (json.isValid(content)) {
let data: object = json.parse(content);
// Process data here
processedCount++;
} else {
errors[errors.length] = "Invalid JSON in ${file}";
}
} else {
errors[errors.length] = "File not found: ${file}";
}
}
catch {
errors[errors.length] = "Error processing ${file}";
}
}
console.log("Processed ${processedCount} files successfully");
if (errors.length > 0) {
console.log("Errors encountered:");
for (let error: string in errors) {
console.log("- ${error}");
}
}
User Input Validation
args.define("--count", "-c", "Number of items", "number", false, 10);
args.define("--name", "-n", "User name", "string", true);
let count: number = args.get("--count");
let userName: string = args.get("--name");
// Validate input
if (count <= 0) {
console.log("Count must be positive");
exit(1);
}
if (userName.length == 0) {
console.log("Name cannot be empty");
exit(1);
}
console.log("Processing ${count} items for user: ${userName}");
Next Steps
Now that you understand variables and types in Utah:
- Learn about functions - Function declarations and parameters
- Explore control flow - Conditionals, loops, and logic
- Study arrays - Array manipulation and operations
- Study strings - String processing and manipulation
- Discover built-in functions - Leverage Utah's function library