Study
11 sheets · 331 questionsPinned
29 questions | entry to principal | answers written to be said out loud, not read
page.tsx makes a route segment publicly reachable, it is the leaf UI for that URL. layout.tsx wraps that page and any nested routes, and it stays mounted across navigations inside it so state like a sidebar or scroll position survives. Every folder in the app directory maps to a URL segment, and only page.tsx or route.ts actually makes it navigable, a folder with just a layout is not a page on its own.
loading.tsx is shown automatically while a Server Component in that segment is fetching data, React just wraps the segment in Suspense for you. error.tsx catches rendering errors in that segment and its children without a manual try catch in every component, and it has to be a Client Component because it uses a reset function. not-found.tsx renders when I call the notFound function or navigate to a path Next cannot match, and template.tsx behaves like layout.tsx but remounts on every navigation instead of persisting.
A Server Component renders on the server, it can read a database or the filesystem directly, and its code never ships to the browser. A Client Component renders on the client too, it can use state, effects, and browser APIs, and its code does end up in the JavaScript bundle. Everything in the App Router is a Server Component by default, I only add the client directive when I actually need interactivity or a browser only API.
It automatically resizes and serves images in a modern format like webp based on the device requesting them, so I am not shipping a huge desktop image to a phone. It lazy loads images outside the viewport by default and reserves the right amount of space up front using the width and height I give it, which avoids layout shift. For anything above the fold, like a hero image, I still have to mark it priority myself or it will lazy load and hurt my largest contentful paint.
It downloads a font at build time and self hosts it alongside my app instead of the browser making a separate request to something like Google Fonts, so there is no extra network round trip and no third party tracking request. It also sets the right font metrics up front to avoid layout shift while the font loads. I import it once, call the loader with the weights I need, and apply the resulting className to my root layout.
A plain environment variable is only available on the server, inside Server Components, route handlers, and Server Actions, and it never reaches the browser bundle. Prefixing one with NEXT_PUBLIC bakes it into the client bundle at build time, so it is genuinely public and I never put a secret behind that prefix. I keep real secrets like database credentials and API keys as server only variables and only expose the handful of values, like a public analytics id, that are actually safe for anyone to see.
route.ts defines a request handler for a segment, I export functions named after HTTP methods like GET or POST and it behaves like an API endpoint rather than a page. I reach for a route handler when something outside my own app needs to call it, like a webhook, a public API, or an OAuth callback. For a form inside my own app I usually prefer a Server Action, it skips writing a fetch call and a matching endpoint.
It marks the boundary where a module and everything it imports gets included in the client bundle and becomes interactive in the browser. It does not mean the component only renders on the client, a Client Component still gets server rendered for the first paint and then hydrates. I put the directive as low as possible in the tree, on the actual interactive piece, so I do not drag a whole page into the client bundle by putting it in a layout.
34 questions | entry to principal | answers written to be said out loud, not read
I default to const because it prevents reassignment and makes intent clear. I use let when reassignment is required. I generally avoid var because it is function-scoped and its hoisting behavior can make code less predictable. let and const are block-scoped.
Scope determines where a variable can be accessed. JavaScript has global, function, and block scope. let and const respect block scope, which helps keep values limited to where they belong.
A closure is when a function keeps access to variables from the scope where it was created, even after that outer scope has finished. Closures matter heavily in React because event handlers, effects, and Hooks capture values from renders.
JavaScript creates bindings for declarations before normal execution. Function declarations can be used before their declaration. var is hoisted and initialized to undefined. let and const exist before initialization too, but accessing them early throws because of the temporal dead zone.
Spread expands values from an array or object. In React I commonly use it to make shallow copies for immutable updates, such as setUser({ ...user, name: 'Mike' }) or setUsers([...users, newUser]).
Destructuring extracts values from arrays or objects into variables. React uses it constantly for props and Hooks, such as const [count, setCount] = useState(0) or const { name } = user.
I think of a function component as a function of props and state that describes UI. Components should stay predictable during rendering and can be composed into larger components and applications.
Props are read-only inputs supplied by a parent. State is data owned by a component that can change over time. I keep state local when possible and lift it when multiple components need to coordinate around it.
30 questions | entry to principal | answers written to be said out loud, not read
TypeScript compares shapes, not names. If two types have the same properties, they are compatible even if they were declared completely separately. This is different from nominal typing in languages like Java, where two classes with identical fields are still different types. It means I can pass a plain object anywhere an interface is expected, as long as the shape matches.
any turns off type checking completely, so I try to avoid it because it lets errors slip through silently. unknown also accepts anything, but it forces me to narrow the value before I can use it, so it is the safe version of any. never represents a value that can't happen, like the return type of a function that always throws, and I use it to make sure a switch statement handles every case.
A union with the pipe means a value can be one of several types, so I have to narrow before I can safely access anything specific to just one of them. An intersection with the ampersand combines multiple types into one that has to satisfy all of them at once. I use unions constantly for things like status fields, and intersections mostly for composing smaller object shapes together.
A literal type narrows a primitive down to one specific value, so instead of just string I can say the type is exactly 'GET' or 'POST'. They're useful for modeling a fixed set of allowed values without reaching for an enum, and they combine naturally with unions to build something like a Method type. They also make autocomplete and typo catching much better than a plain string.
Generics let me write a function, type, or component that works across multiple types while keeping the relationship between the input and output type safe. Instead of typing a parameter as any, I give it a type parameter like T, and TypeScript infers what T actually is at the call site. The classic example is a function that returns the first element of an array, where the return type should match whatever array you passed in, not some fixed type.
Partial makes every property optional, which I use a lot for update payloads where a caller only sends the fields they're changing. Pick and Omit both build a smaller type from an existing one, Pick by keeping a specific list of keys and Omit by dropping them. Record builds an object type from a union of keys mapped to a value type, which is perfect for something like a lookup table keyed by an id or a status.
Narrowing is TypeScript refining a broad type down to something more specific based on checks in my code, like typeof, instanceof, or an equality check. A type guard is a function that returns a boolean and tells the compiler that if it returns true, the value is a specific type. I write custom type guards with an is predicate when the built in checks aren't enough, like checking for a specific shape on an unknown API response.
A discriminated union is a set of object types that all share a common literal field, like a kind or status property, that tells TypeScript which variant it's looking at. Once I check that field in an if or switch, TypeScript narrows the whole object automatically, so I get full type safety without a single cast. I use this pattern constantly for things like API responses or state machines with success and error branches.
Everything else
30 questions | entry to principal | answers written to be said out loud, not read
A shell is a program that reads commands I type, or lines from a script, and asks the operating system to run them. It handles parsing, expansion, piping processes together, and reporting back results. Bash is one specific shell, and the terminal is just the window I'm typing into, not the shell itself.
The shebang, like the env bash line at the very top, tells the kernel which interpreter to hand the rest of the file to when it's executed directly. I use env bash instead of a hardcoded bin bash path because it looks up bash on the current PATH, which matters across machines where bash lives in different places. Without a shebang and execute permission, the file just runs in whatever shell invoked it.
Every command returns an exit code when it finishes, 0 for success and anything from 1 to 255 for some kind of failure, and the meaning of a nonzero code is defined by that specific program. The question-mark variable holds the exit code of the last command that ran, so I check it immediately before running anything else that would overwrite it. In scripts I use that to branch on success or failure, or to propagate a meaningful code back to whatever called the script.
PATH is a colon-separated list of directories the shell searches in order to resolve a bare command name to an executable. Environment variables are inherited by child processes when they're exported, while a plain shell variable stays local to the current shell. To check whether a command is available before relying on it, I use command -v rather than which, because command -v is a builtin and works consistently across shells.
Command substitution runs a command and swaps in its standard output as text, trimming trailing newlines. I always use the dollar-paren form over backticks because it's easier to read and nests cleanly without escaping. It's how I capture things like the current git branch or a timestamp into a variable for later use.
A function groups commands under a name I can call like any other command. Arguments come in positionally inside the function, just like script arguments, and an at-sign expansion holds all of them. A function's exit status is the exit status of its last command unless I return an explicit code, and return only accepts numbers 0 to 255.
for iterates over a list of words, a range, or array elements. while runs as long as a condition stays true, which is what I reach for when reading lines from a file or a command's output. until is the inverse of while, running until the condition becomes true. For reading files line by line I use a while loop with read -r, because that avoids word splitting and preserves backslashes in each line.
cut pulls out columns by delimiter or fixed character position when I don't need awk's full power. sort orders lines, and uniq collapses adjacent duplicate lines, so I almost always pipe through sort first since uniq only catches duplicates that are next to each other. tr does simple character-level translation or deletion, like squashing repeated whitespace or converting a file to lowercase. They're small, but chaining them together covers a surprising amount of everyday text reshaping.
30 questions | entry to principal | answers written to be said out loud, not read
Node is a JavaScript runtime built on Chrome's V8 engine that runs outside the browser. My code runs on a single main thread, and the event loop keeps that thread from blocking by handing off I/O work like file reads and network calls, then running my callback once the result comes back. That is why Node handles thousands of concurrent connections well even though only one thread executes my JavaScript.
Blocking code runs synchronously and holds up the single thread until it finishes, so something like a synchronous file read or a heavy loop stalls every other request. Non-blocking code kicks off the work and lets the event loop keep serving other requests while it waits, then runs a callback when it's done. In practice that means I reach for the async version of an API and I never run CPU-heavy work directly on the main thread.
Express is a minimal web framework on top of Node's built-in http module. It gives me routing, middleware, and a request and response API without dictating much else, so it stays lightweight and flexible. I reach for it when I want control over the structure of an API rather than the conventions a bigger framework would impose on me.
Middleware is just a function with req, res, and next that runs in the middle of the request and response. It can inspect or modify the request, end the response, or call next to pass control to the next function in the chain. Order matters because Express runs middleware in the exact sequence I register it, so something like authentication has to come before the route handler that needs it, and a logging middleware placed after the response is sent won't do anything useful.
A request comes in, and Express matches it against my registered routes in order. It runs through any matching middleware, then the route handler, and each step either calls next to continue or ends the cycle by calling something like res.send or res.json. If nothing ever ends the response, the client just hangs waiting, which is a bug I've had to track down before.
I try to be precise instead of just returning 200 or 500 for everything. 2xx means success, 400 means the client sent something invalid, 401 means they're not authenticated, 403 means they are authenticated but not allowed, 404 means the resource doesn't exist, and 500 means something broke on my end. Getting this right matters because clients, monitoring, and caching all key off the status code.
A Buffer is a fixed-size chunk of raw binary data sitting outside the V8 heap. I run into it constantly with streams, file I/O, and network protocols, since data arrives as bytes before it gets decoded into a string or JSON. I have to know the encoding, usually utf-8, when I convert a Buffer to a string so I don't corrupt multi-byte characters.
CommonJS uses require and module.exports, loads synchronously, and has been the default in Node for years. ES modules use import and export, support static analysis and tree-shaking, and are the standard going forward, either in a .mjs file or a package.json with type module. The two systems don't mix cleanly, an ESM file can import CommonJS but a CommonJS file can't require an ESM package directly, so I pick one per project and I'm careful when I add a dependency that's ESM-only.
30 questions | entry to principal | answers written to be said out loud, not read
Go came out of frustration with slow C++ builds, sprawling dependency graphs, and how hard it was to onboard engineers onto huge codebases at Google. The designers wanted the safety and garbage collection of a managed language but the fast compilation and simplicity of C. So Go leans hard into a small language spec, fast builds, and built-in concurrency instead of giving you 5 competing paradigms to choose between.
No, and that's intentional. Go randomizes map iteration order on purpose specifically so nobody writes code that accidentally depends on it. If I need a deterministic order I pull the keys into a slice, sort that slice, and then iterate the slice instead of ranging over the map directly.
A struct is just a typed collection of fields, a plain data container with no inheritance and no constructor built into the language. I attach behavior to it by defining methods on the type separately, and I initialize it with a plain literal or a small constructor function like NewUser if there's setup logic. It's deliberately less ceremony than a class in an object oriented language.
Every type in Go has a zero value, so a variable is usable the moment it's declared even if you never assign to it. An int zero value is 0, a bool is false, a pointer or slice or map or channel is nil, and a struct's zero value is just each field set to its own zero value. I lean on this a lot, for example a freshly declared struct is immediately safe to read fields from without a null check.
A Go interface is just a set of method signatures, and a type satisfies it automatically the moment it implements those methods, there's no implements keyword and no explicit declaration required. That's structural typing rather than nominal typing, so I can define a small interface after the fact and any existing type that happens to have the right methods already satisfies it. It's why Go interfaces tend to be small, often just 1 or 2 methods, because you're describing behavior you need rather than declaring a formal contract up front.
In Go an error is just an ordinary value that implements the error interface, and functions that can fail return it as an explicit extra return value instead of throwing. The philosophy is that error handling should be visible in the code path rather than jumping through an invisible control flow, so I see if err != nil right where the failure can actually happen. It reads as more verbose, but I actually like that I can't accidentally swallow an error the way you can with an empty catch block.
A goroutine is a function running concurrently, managed by the Go runtime instead of the operating system, and starting one is as cheap as writing go before a function call. It starts with a tiny stack, only a few kilobytes, that grows and shrinks as needed, so I can comfortably spin up thousands or even millions of them, which isn't realistic with OS threads that cost megabytes each. The runtime multiplexes many goroutines onto a much smaller number of actual OS threads.
net/http alone gives you a production capable HTTP server and client, routing, and TLS without pulling in a single external dependency, and that's genuinely what a lot of real services ship on directly. Beyond that, encoding/json, database/sql, context, and testing cover most of what a typical backend service needs day to day. I still reach for a router library or an ORM sometimes for convenience, but it's a real choice rather than a requirement, which is different from ecosystems where you basically can't start without picking a framework first.
29 questions | entry to principal | answers written to be said out loud, not read
I reach for const by default because it signals the binding won't be reassigned, and I use let when I know a value needs to change. var is function-scoped and gets hoisted and initialized to undefined. let and const are block-scoped and they're hoisted too, but they sit in the temporal dead zone until their declaration actually runs, so touching them earlier throws a reference error instead of silently giving you undefined.
Destructuring lets me pull values out of arrays or objects straight into named variables instead of accessing them one property at a time. I use it constantly for function parameters, for pulling specific fields off an API response, and for swapping variables without a temporary one. I also lean on default values and renaming inside the same expression when the source shape doesn't match what I want to call things.
Default parameters let a function fall back to a value when an argument is missing or undefined, so I don't need manual checks inside the body. Rest collects the remaining arguments into a real array, which replaced the old arguments object for me. Spread does the opposite, it expands an array or object out into individual elements or properties, and I use it constantly for shallow copies and merging.
Template literals let me embed expressions directly inside a string with the dollar-brace syntax, so I'm not chaining a bunch of plus signs together. They also support real multi-line strings without escape characters, which is a small thing but it makes generated HTML or SQL far more readable. On top of that they're the foundation for tagged templates, where a function gets to process the literal before it becomes a string.
map transforms every element and returns a new array of the same length. filter keeps only the elements that pass a test and returns a new, possibly shorter array. reduce is the general-purpose one, it walks the array with an accumulator and can build up anything, a sum, an object, a grouped structure, or even another array, so I reach for it whenever map and filter alone can't express what I need.
A Promise represents a value that isn't available yet but will resolve or fail at some point in the future. It starts pending, and it settles exactly once into either fulfilled with a value or rejected with a reason, it can never go back to pending or flip from one settled state to the other. I think of a Promise as a container I can attach then and catch handlers to instead of passing callbacks around by hand.
A closure is what happens when a function remembers the variables from the scope it was defined in, even after that outer function has finished running. I use them all the time for things like a private counter, memoization caches, or debounce and throttle utilities where I need state that persists between calls but isn't exposed globally. The inner function just keeps a live reference to those outer variables instead of copying their values.
An arrow function doesn't get its own this, it just inherits this from the surrounding lexical scope where it was written. A regular function's this depends on how it's called, which is what causes bugs inside callbacks and event handlers. That's why I reach for arrow functions for things like array callbacks or class methods where I want this to stay tied to the enclosing context.
30 questions | entry to principal | answers written to be said out loud, not read
A service provider is where Laravel bootstraps things. It has a register method for binding things into the container and a boot method for code that should run once everything is registered, like defining routes, view composers, or event listeners. Every core Laravel feature, and every package, gets wired up through a provider.
Instead of manually looking up a model by an id in the controller, I type-hint the model in the route signature and Laravel resolves it for me based on the route parameter. If no matching record exists it throws a 404 automatically. I can also bind on a different column, like a slug, by overriding getRouteKeyName on the model.
Blade is Laravel's templating engine. It compiles down to plain PHP, so there's no real runtime overhead, but it gives me cleaner syntax for things like conditionals, loops, layouts, and component includes. I like that {{ }} escapes output by default, which protects against XSS unless I deliberately opt out with the raw {!! !!} echo syntax.
An Eloquent model is the active record representation of a database table, so each instance maps to a row and the class itself represents the table. I use it to query, create, and update data with an expressive API instead of writing raw SQL everywhere. I also guard against mass assignment by explicitly listing fillable or guarded attributes, so a stray input field can't overwrite something like an is_admin column.
Migrations are version control for the database schema, so every environment ends up with the exact same tables and columns, and I can roll one back if it turns out to be wrong. Seeders populate the database with data I want to exist, like default roles or an admin user. Factories generate fake data for testing and local development, and I usually combine the two, calling a factory from a seeder to fill the database with realistic sample records.
Instead of managing a pile of individual cron entries on the server, I define all my recurring tasks in one schedule, like a command that runs daily or a job that runs every 5 minutes. The only cron entry the server actually needs is one that runs php artisan schedule:run every minute, and Laravel figures out what's actually due to run.
A request comes in through public/index.php, which boots the framework and hands the request to the HTTP kernel. The kernel runs the request through global middleware and route middleware, then the router matches it to a controller or closure. Laravel resolves that controller's dependencies out of the container, runs the action, and the response travels back out through the middleware stack before it's sent to the browser.
I type-hint the class or interface I need in the constructor or method signature, and Laravel's container inspects that signature and resolves each dependency automatically, including nested dependencies. For route model binding, Laravel goes further and resolves the actual model instance from the route parameter. I never call new directly on a service I want the container to manage.
29 questions | entry to principal | answers written to be said out loud, not read
=== compares both value and type with no conversion, and that is what I reach for by default. == first coerces one side to match the other's type before comparing, which is where classic surprises like 0 == 'abc' being true in old PHP versions came from, though PHP 8 fixed several of the worst string-to-number comparison cases. Type juggling in general means PHP will silently convert types in arithmetic, comparisons, and string contexts, and I treat unexpected juggling as a code smell.
Scalar type declarations let me put int, float, string, and bool right on function parameters and return types, so PHP validates or coerces the value instead of me checking it by hand. Union types, added in PHP 8, let a parameter or return type accept more than one type, written as int|string for example, which is honest when a function genuinely handles more than one shape of input. I prefer a narrow union or a single type over no type at all, because the type declaration is documentation the interpreter actually enforces.
match uses strict comparison, has no fall-through, requires every branch to be exhaustive or have a default, and is an expression that returns a value directly. switch uses loose comparison, falls through unless I remember a break on every case, and is a statement, not an expression. I default to match for anything that maps an input to a value, and I only reach for switch when I genuinely need multiple cases to share one block of logic.
Before PHP 8, giving a class a typed property meant declaring the property, adding a matching constructor parameter, and then writing $this->name = $name for every single one. Constructor promotion lets me put the visibility modifier directly on the constructor parameter, and PHP declares the property and assigns it for me in one line. I use it by default now for simple data-holding classes and DTOs, it removes boilerplate without hiding any real behavior.
Named arguments let me pass a value by matching the parameter's name instead of its position, so I write createUser(email: $email, isAdmin: true) instead of counting positions. They really pay off with functions that have several optional parameters, because I can skip the ones I do not need instead of passing default placeholders just to reach the one I care about. They also make a call site self-documenting, anyone reading it sees exactly which value maps to which parameter without opening the function signature.
A readonly property can be set exactly once, typically inside the constructor, and any attempt to modify it afterward throws an Error, even from inside the class itself. That gives me real immutability for value objects and DTOs without hand-writing a bunch of get-only accessors and defensive checks. I use readonly constantly now for things like money amounts, ids, and configuration values that should never change after construction.
PHP-FPM is the process manager that keeps a pool of worker processes warm so Nginx or Apache does not have to spawn a new PHP process per request, and it lets me tune how many workers exist and how they scale. Opcache caches the compiled bytecode of my PHP files in shared memory so PHP does not have to re-parse and recompile the same source on every single request. Together they are why PHP in production feels nothing like running php script.php from the command line, and turning opcache off in production is a real performance mistake.
Without it, PHP will coerce scalar arguments and return values to match a function's declared types, so passing the string '5' to a function expecting int just silently works. With strict_types declared at the top of a file, that coercion is disabled for scalar type declarations in that file, and a mismatched type throws a TypeError instead of being converted. I turn it on in new code because I want type mismatches caught immediately instead of silently converted into a bug three layers deeper.
30 questions | entry to principal | answers written to be said out loud, not read
Immutable objects like int, str, tuple, and frozenset cannot be changed after creation, so any operation that looks like a mutation actually creates a new object. Mutable objects like list, dict, and set can be changed in place, which means multiple references can see the same change. I care about this mostly around function arguments and default values, because passing a mutable object hands out a shared reference, not a copy.
I use a list when I need an ordered, mutable sequence. A tuple is for a fixed, ordered group of values, often heterogeneous, and it's hashable if its contents are, so it can be a dict key. A set is for unique, unordered membership checks and set algebra like union and intersection. A dict is for key to value lookups where I need fast access by a meaningful key instead of a position.
A generator is a function that uses yield instead of return, and calling it gives back an iterator without running any code yet. Each call to next pauses and resumes the function at the yield statement, keeping its local state alive between calls. That lets me produce a sequence of values lazily without building the whole thing in memory up front.
is checks identity, meaning both names point to the exact same object in memory. == checks equality, meaning the values compare equal, which can be true even for two different objects. I use is for None, True, and False checks because those are singletons, and == for comparing actual values like strings or numbers.
*args collects any extra positional arguments into a tuple, and **kwargs collects extra keyword arguments into a dict. I use them when writing a function that needs to accept a flexible or unknown number of arguments, or when I'm wrapping another function and just need to forward whatever gets passed in.
A class bundles data and behavior together, gives you a defined shape through __init__, and lets you use inheritance, methods, properties, and dunder methods to control how instances behave. A dict is fine for loose, ad hoc data, but a class documents intent, and tools like mypy or an IDE can check attribute names and types against it in a way they can't for a bag of dict keys.
f-strings let me embed expressions directly inside the string literal, so it's more readable and I can call functions or do arithmetic right where the value is used. They're also evaluated at runtime as bytecode rather than parsed as a separate format string, which makes them faster than .format() or % formatting in practice.
pathlib gives me a Path object with an object oriented API instead of passing strings around to a bunch of separate os.path functions. Joining paths is just the slash operator, and common checks like exists, is_file, or reading text are methods right on the object. It also handles cross platform separators for me, so I don't have to think about forward versus backward slashes.
30 questions | entry to principal | answers written to be said out loud, not read
Ownership is Rust's rule that every value has exactly one owner at a time, and when that owner goes out of scope the value is dropped. It replaces manual memory management and garbage collection with compile time checks. When I assign a value to a new variable or pass it into a function, ownership moves unless the type implements Copy.
Moving transfers ownership from one binding to another, and the original binding becomes invalid so the compiler will reject any further use of it. This avoids double frees because only one owner is ever responsible for cleaning up the value. For types that are cheap to duplicate, like integers, Rust implements Copy instead so no move happens at all.
Borrowing lets me use a value through a reference without taking ownership of it. The borrow checker enforces that I can have either one mutable reference or any number of immutable references to a value at a time, never both together. That rule is what prevents data races and use-after-free bugs at compile time instead of at runtime.
Fixed size, known-at-compile-time data like integers and simple structs live on the stack, which is fast to allocate and clean up. Data with a size that can grow, like a String or a Vec, stores its contents on the heap and keeps a pointer, length, and capacity on the stack. Ownership rules apply the same way to both, but heap data is what actually gets freed when the owner goes out of scope.
Vec is a growable, heap allocated, owned collection of values of one type. A slice is a borrowed view into a contiguous sequence, like a Vec or an array, described by a pointer and a length. I use slices constantly in function signatures because they let a function accept a Vec, an array, or part of either without caring about ownership.
Option represents a value that might or might not be present, with variants Some and None, and it replaces null entirely. Result represents an operation that can succeed or fail, with variants Ok and Err carrying a value or an error. Both are enums the compiler forces me to handle, so I cannot accidentally skip the failure or empty case the way I could with null or an unchecked exception.
A struct groups related named fields into one type, similar to a class without inheritance. I attach behavior to a struct through impl blocks rather than defining methods inside the struct itself, and I compose functionality through traits instead of subclassing. Rust also has tuple structs and unit structs for lighter weight cases where named fields aren't necessary.
A trait defines a set of methods a type must implement, which is how Rust does shared behavior instead of inheritance. I implement a trait for a type with an impl block, and I can require a generic function to accept any type that implements a given trait. Traits can also provide default method implementations, so implementers only override what they need to.