{"id":9734,"date":"2025-07-23T14:35:00","date_gmt":"2025-07-23T11:35:00","guid":{"rendered":"https:\/\/handoli.com\/index.php\/2025\/07\/23\/deciding-between-let-and-var-for-swift-struct-properties\/"},"modified":"2025-07-23T14:35:00","modified_gmt":"2025-07-23T11:35:00","slug":"deciding-between-let-and-var-for-swift-struct-properties","status":"publish","type":"post","link":"https:\/\/handoli.com\/index.php\/2025\/07\/23\/deciding-between-let-and-var-for-swift-struct-properties\/","title":{"rendered":"Deciding between \u2018let\u2019 and \u2018var\u2019 for Swift struct properties"},"content":{"rendered":"<p>When declaring a Swift property, we use either the <code>let<\/code> or <code>var<\/code> keyword depending on whether we want our new property to be read-only once assigned through the enclosing type\u2019s initializer, or whether we want to allow the property to be mutated and re-assigned multiple times.<\/p>\n<p>However, that\u2019s not the only difference between using <code>let<\/code> versus <code>var<\/code> when working with Swift structs \u2014 as both approaches also influence the enclosing struct\u2019s behaviors in various ways. Let\u2019s explore!<\/p>\n<h2>The side-effects of immutability<\/h2>\n<p>Let\u2019s say that we\u2019ve declared a <code>User<\/code> struct within a project, which currently only contains constant <code>let<\/code> properties:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">struct<\/span> User: <span class=\"s-type\">Identifiable<\/span>, <span class=\"s-type\">Codable<\/span> {\n    <span class=\"s-keyword\">let<\/span> id: <span class=\"s-type\">UUID<\/span>\n    <span class=\"s-keyword\">let<\/span> name: <span class=\"s-type\">String<\/span>\n    <span class=\"s-keyword\">let<\/span> bio: <span class=\"s-type\">String<\/span>\n    <span class=\"s-keyword\">let<\/span> imageURL: <span class=\"s-type\">URL<\/span>?\n}<\/code><\/pre>\n<p>Besides the fact that neither of those properties can be directly modified when working with a mutable <code>User<\/code> value, marking <code>imageURL<\/code> specifically as a <code>let<\/code> actually influences how our struct\u2019s initializer behaves.<\/p>\n<p>When an optional struct property is declared as a <code>let<\/code>, then we\u2019re <em>always required<\/em> to pass a value for it when using the default compiler-generated, so-called <em>member-wise<\/em> initializer. That means that even in situations when a user\u2019s <code>imageURL<\/code> should be <code>nil<\/code>, we have to explicitly specify that:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">let<\/span> user = <span class=\"s-type\">User<\/span>(\n    id: <span class=\"s-type\">UUID<\/span>(),\n    name: <span class=\"s-string\">\"John Appleseed\"<\/span>,\n    bio: <span class=\"s-string\">\"Famous person within the Apple Cinematic Universe\"<\/span>,\n    imageURL: <span class=\"s-keyword\">nil<\/span>\n)<\/code><\/pre>\n<blockquote>\n<p>If <code>imageURL<\/code> was declared as a <code>var<\/code> instead, then we could\u2019ve simply omitted that parameter above.<\/p>\n<\/blockquote>\n<p>Whether that\u2019s an advantage or disadvantage likely depends on the situation (and personal taste). Sometimes it\u2019s great that we can\u2019t forget to pass an <code>imageURL<\/code>, and sometimes the above just leads to unnecessary boilerplate.<\/p>\n<p>Another way that <code>let<\/code> properties differ from <code>var<\/code>-declared ones is when it comes to <em>default values<\/em>, since <code>let<\/code> properties treat such values as <em>constant declarations<\/em>. For example, let\u2019s say that we wanted to add a default value for our <code>User<\/code> struct\u2019s <code>id<\/code> property \u2014 to avoid having to manually pass <code>UUID()<\/code> every time we create a new user:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">struct<\/span> User: <span class=\"s-type\">Identifiable<\/span>, <span class=\"s-type\">Codable<\/span> {\n    <span class=\"s-keyword\">let<\/span> id = <span class=\"s-type\">UUID<\/span>()\n    <span class=\"s-keyword\">let<\/span> name: <span class=\"s-type\">String<\/span>\n    <span class=\"s-keyword\">let<\/span> bio: <span class=\"s-type\">String<\/span>\n    <span class=\"s-keyword\">let<\/span> imageURL: <span class=\"s-type\">URL<\/span>?\n}<\/code><\/pre>\n<p>If <code>id<\/code> was a <code>var<\/code>, then the above change would simply mean that our <code>UUID()<\/code> expression would be used <em>unless<\/em> we pass an explicit value for that parameter (either when directly initializing <code>User<\/code>, or when decoding a value from a data format, such as JSON). However, since it\u2019s a <code>let<\/code>, it now means that we <em>can\u2019t<\/em> actually pass a value for that property at all \u2014 the <code>UUID()<\/code> expression will always be used, and there\u2019s no way to override that.<\/p>\n<p>Since our <code>User<\/code> type also conforms to <code>Decodable<\/code> (through the <code>Codable<\/code> type alias), that actually also means that no <code>id<\/code> value will ever be decoded, and any such value that\u2019s present within the JSON (or other data format) that we\u2019re decoding from will simply be ignored. In fact, the Swift compiler will even give us a warning when using the above pattern, since it\u2019s likely not the decoding behavior we want our type to have.<\/p>\n<h2>Manually declared initializers<\/h2>\n<p>So what if we wanted to change some of the behaviors that we explored above? One way to do that would be to manually declare our type\u2019s initializer, rather than relying on the member-wise one that the compiler generates for us. For example, let\u2019s say that we wanted to allow call sites to omit the <code>imageURL<\/code> property if it should simply be <code>nil<\/code> (without changing it to a <code>var<\/code>), and\/or use a default <code>UUID<\/code> for the <code>id<\/code> property if no explicit value was passed. That could be done like this:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">struct<\/span> User: <span class=\"s-type\">Identifiable<\/span>, <span class=\"s-type\">Codable<\/span> {\n    <span class=\"s-keyword\">let<\/span> id: <span class=\"s-type\">UUID<\/span>\n    <span class=\"s-keyword\">let<\/span> name: <span class=\"s-type\">String<\/span>\n    <span class=\"s-keyword\">let<\/span> bio: <span class=\"s-type\">String<\/span>\n    <span class=\"s-keyword\">let<\/span> imageURL: <span class=\"s-type\">URL<\/span>?\n\n    <span class=\"s-keyword\">init<\/span>(id: <span class=\"s-type\">UUID<\/span> = <span class=\"s-type\">UUID<\/span>(),\n         name: <span class=\"s-type\">String<\/span>,\n         bio: <span class=\"s-type\">String<\/span>,\n         imageURL: <span class=\"s-type\">URL<\/span>? = <span class=\"s-keyword\">nil<\/span>) {\n        <span class=\"s-keyword\">self<\/span>.<span class=\"s-property\">id<\/span> = id\n        <span class=\"s-keyword\">self<\/span>.<span class=\"s-property\">name<\/span> = name\n        <span class=\"s-keyword\">self<\/span>.<span class=\"s-property\">bio<\/span> = bio\n        <span class=\"s-keyword\">self<\/span>.<span class=\"s-property\">imageURL<\/span> = imageURL\n    }\n}<\/code><\/pre>\n<p>The above strikes a quite nice balance between maintaining immutability (if that\u2019s something we want), while still enabling convenience features like default values \u2014 at the cost of having to manually write and maintain our own explicit initializer.<\/p>\n<h2>A property wrapper alternative<\/h2>\n<p>An alternative approach to the above, which may be something we want to consider if we want to deploy the <em>constants-with-default-values<\/em> pattern in many places across a larger code base, is to use a property wrapper to make <code>var<\/code> properties <em>read-only<\/em>.<\/p>\n<p>Since the mutability of a wrapped property depends on the wrapper itself, we could declare a <code>Readonly<\/code> wrapper type which marks its <code>wrappedValue<\/code> as a <code>let<\/code> \u2014 like this:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">@propertyWrapper struct<\/span> Readonly&lt;Value&gt; {\n    <span class=\"s-keyword\">let<\/span> wrappedValue: <span class=\"s-type\">Value<\/span>\n}<\/code><\/pre>\n<p>Since we\u2019re planning to use our <code>Readonly<\/code> wrapper within types that conform to <code>Encodable<\/code> and <code>Decodable<\/code>, then we also need to adopt those protocols for our wrapper as well \u2014 since all coding tasks are deferred to property wrappers when they\u2019re used:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">extension<\/span> <span class=\"s-type\">Readonly<\/span>: <span class=\"s-type\">Encodable<\/span> <span class=\"s-keyword\">where<\/span> <span class=\"s-type\">Value<\/span>: <span class=\"s-type\">Encodable<\/span> {\n    <span class=\"s-keyword\">func<\/span> encode(to encoder: <span class=\"s-type\">Encoder<\/span>) <span class=\"s-keyword\">throws<\/span> {\n        <span class=\"s-keyword\">var<\/span> container = encoder.<span class=\"s-call\">singleValueContainer<\/span>()\n        <span class=\"s-keyword\">try<\/span> container.<span class=\"s-call\">encode<\/span>(wrappedValue)\n    }\n}\n\n<span class=\"s-keyword\">extension<\/span> <span class=\"s-type\">Readonly<\/span>: <span class=\"s-type\">Decodable<\/span> <span class=\"s-keyword\">where<\/span> <span class=\"s-type\">Value<\/span>: <span class=\"s-type\">Decodable<\/span> {\n    <span class=\"s-keyword\">init<\/span>(from decoder: <span class=\"s-type\">Decoder<\/span>) <span class=\"s-keyword\">throws<\/span> {\n        <span class=\"s-keyword\">let<\/span> container = <span class=\"s-keyword\">try<\/span> decoder.<span class=\"s-call\">singleValueContainer<\/span>()\n        wrappedValue = <span class=\"s-keyword\">try<\/span> container.<span class=\"s-call\">decode<\/span>(<span class=\"s-type\">Value<\/span>.<span class=\"s-keyword\">self<\/span>)\n    }\n}<\/code><\/pre>\n<blockquote>\n<p>Above we\u2019re using Swift\u2019s conditional conformances feature to not have to require that all <code>Readonly.Value<\/code> types always have to conform to <code>Encodable<\/code> and <code>Decodable<\/code>, which would limit our property wrapper\u2019s versatility.<\/p>\n<\/blockquote>\n<p>With the above in place, we can now go ahead and update our <code>User<\/code> type to use <code>Readonly<\/code>-marked <code>var<\/code> declarations for the properties that we want to define a default value for:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">struct<\/span> User: <span class=\"s-type\">Identifiable<\/span>, <span class=\"s-type\">Codable<\/span> {\n    <span class=\"s-keyword\">@Readonly var<\/span> id = <span class=\"s-type\">UUID<\/span>()\n    <span class=\"s-keyword\">let<\/span> name: <span class=\"s-type\">String<\/span>\n    <span class=\"s-keyword\">let<\/span> bio: <span class=\"s-type\">String<\/span>\n    <span class=\"s-keyword\">@Readonly var<\/span> imageURL: <span class=\"s-type\">URL<\/span>?\n}<\/code><\/pre>\n<blockquote>\n<p>Note that, since we\u2019re now using a <code>var<\/code> for our type\u2019s <code>imageURL<\/code> property, it\u2019s default value automatically becomes <code>nil<\/code> \u2014 there\u2019s no need for us to declare that manually.<\/p>\n<\/blockquote>\n<p>The advantage of the above approach is that we now have a reusable solution that helps us avoid having to manually declare initializers when all we want is to be able to define default values for read-only properties. However, whenever we use a non-standard solution, like the one above, it\u2019s important to consider whether the inherent additional complexity of such a custom solution is worth the benefits that we get from it.<\/p>\n<h2>Is immutability always the answer?<\/h2>\n<p>Another alternative approach to make our properties support default values is to simply declare them using <code>var<\/code> instead. While that <em>does<\/em> enable those properties to be mutated, perhaps that isn\u2019t actually a problem, given that all structs are by default passed as immutable copies when calling a function or when initializing another type.<\/p>\n<p>In practice, that means that structs don\u2019t have the same <em>shared mutable state<\/em> problem that classes often do (unless static mutable values are used), so the question is how problematic it would actually be to do something like this:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">struct<\/span> User: <span class=\"s-type\">Identifiable<\/span>, <span class=\"s-type\">Codable<\/span> {\n    <span class=\"s-keyword\">var<\/span> id = <span class=\"s-type\">UUID<\/span>()\n    <span class=\"s-keyword\">var<\/span> name: <span class=\"s-type\">String<\/span>\n    <span class=\"s-keyword\">var<\/span> bio: <span class=\"s-type\">String<\/span>\n    <span class=\"s-keyword\">var<\/span> imageURL: <span class=\"s-type\">URL<\/span>?\n}<\/code><\/pre>\n<p>One <em>benefit<\/em> of making our structs as mutable as possible is that doing so often makes it much easier to write unit tests \u2014 either when our structs are used as stub values, or when the structs themselves are the types being tested. For example, let\u2019s say that we wanted to add a method for normalizing a given user\u2019s name:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">extension<\/span> <span class=\"s-type\">User<\/span> {\n    <span class=\"s-keyword\">mutating func<\/span> normalizeName() {\n        name = name\n            .<span class=\"s-call\">filter<\/span> { char <span class=\"s-keyword\">in<\/span>\n                char.<span class=\"s-property\">isLetter<\/span> || (char.<span class=\"s-property\">isWhitespace<\/span> &amp;&amp; !char.<span class=\"s-property\">isNewline<\/span>)\n            }\n            .<span class=\"s-call\">trimmingCharacters<\/span>(in: .<span class=\"s-dotAccess\">whitespaces<\/span>)\n    }\n}<\/code><\/pre>\n<p>Since the <code>name<\/code> property is now a <code>var<\/code>, we could easily write a test that verifies various normalization scenarios, all while reusing the same <code>User<\/code> value \u2014 for example like this:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">struct<\/span> UserTests {\n    <span class=\"s-keyword\">@Test func<\/span> normalizingName() {\n        <span class=\"s-keyword\">var<\/span> user = <span class=\"s-type\">User<\/span>(name: <span class=\"s-string\">\"Name\"<\/span>, bio: <span class=\"s-string\">\"Bio\"<\/span>)\n\n        <span class=\"s-comment\">\/\/ Non-letter characters are removed:<\/span>\n        user.<span class=\"s-property\">name<\/span> = <span class=\"s-string\">\"!1_First 2;Last_3?\"<\/span>\n        user.<span class=\"s-call\">normalizeName<\/span>()\n        <span class=\"s-call\">#expect<\/span>(user.<span class=\"s-property\">name<\/span> == <span class=\"s-string\">\"First Last\"<\/span>)\n\n        <span class=\"s-comment\">\/\/ Leading and trailing whitespaces are removed:<\/span>\n        user.<span class=\"s-property\">name<\/span> = <span class=\"s-string\">\" White Spaces \"<\/span>\n        user.<span class=\"s-call\">normalizeName<\/span>()\n        <span class=\"s-call\">#expect<\/span>(user.<span class=\"s-property\">name<\/span> == <span class=\"s-string\">\"White Spaces\"<\/span>)\n    }\n}<\/code><\/pre>\n<p>It\u2019s also important to remember that just because we mark a given struct property as a <code>let<\/code> doesn\u2019t mean that its value can <em>never change<\/em>, since the mutability of a given value is always determined by the top-level, enclosing value that the property is contained within.<\/p>\n<p>For example, let\u2019s say that we wanted to make another attempt at striking a nice balance between mutability and consistency for our <code>User<\/code> type \u2014 this time by making all non-<code>id<\/code> properties variables, while keeping the <code>id<\/code> property a <code>let<\/code> (since that\u2019s the one property we never expect to change throughout the lifetime of a <code>User<\/code> value):<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">struct<\/span> User: <span class=\"s-type\">Identifiable<\/span>, <span class=\"s-type\">Codable<\/span> {\n    <span class=\"s-keyword\">let<\/span> id: <span class=\"s-type\">UUID<\/span>\n    <span class=\"s-keyword\">var<\/span> name: <span class=\"s-type\">String<\/span>\n    <span class=\"s-keyword\">var<\/span> bio: <span class=\"s-type\">String<\/span>\n    <span class=\"s-keyword\">var<\/span> imageURL: <span class=\"s-type\">URL<\/span>?\n}<\/code><\/pre>\n<p>Then, let\u2019s say that we wanted to introduce an API for transforming a given <code>User<\/code> value in some way, for example by using the <code>normalizeName<\/code> method we defined earlier. Such an API could take the form of a <code>UserTransformer<\/code> protocol, which uses Swift\u2019s <code>inout<\/code> parameter feature to enable each transformer to directly mutate the <code>User<\/code> value that was passed to it, without first having to make a mutable copy:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">protocol<\/span> UserTransformer {\n    <span class=\"s-keyword\">func<\/span> transformUser(<span class=\"s-keyword\">_<\/span> user: <span class=\"s-keyword\">inout<\/span> <span class=\"s-type\">User<\/span>)\n}\n\n<span class=\"s-keyword\">struct<\/span> UserNameNormalizer: <span class=\"s-type\">UserTransformer<\/span> {\n    <span class=\"s-keyword\">func<\/span> transformUser(<span class=\"s-keyword\">_<\/span> user: <span class=\"s-keyword\">inout<\/span> <span class=\"s-type\">User<\/span>) {\n        user.<span class=\"s-call\">normalizeName<\/span>()\n    }\n}<\/code><\/pre>\n<p>So the question is, with the above setup, do we have a <em>guarantee<\/em> that the <code>id<\/code> property of any <code>User<\/code> value that was passed to a <code>UserTransformer<\/code> implementation can never be changed? No, actually, we don\u2019t. Because we have to remember, the <code>User<\/code> value <em>itself<\/em> is mutable, and it can be completely re-assigned with a brand new <code>UUID<\/code> if the implementation so desires \u2014 for example like this:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">struct<\/span> UserIDTransformer: <span class=\"s-type\">UserTransformer<\/span> {\n    <span class=\"s-keyword\">func<\/span> transformUser(<span class=\"s-keyword\">_<\/span> user: <span class=\"s-keyword\">inout<\/span> <span class=\"s-type\">User<\/span>) {\n        user = <span class=\"s-type\">User<\/span>(\n            id: <span class=\"s-type\">UUID<\/span>(),\n            name: user.<span class=\"s-property\">name<\/span>,\n            bio: user.<span class=\"s-property\">bio<\/span>,\n            imageURL: user.<span class=\"s-property\">imageURL<\/span>\n        )\n    }\n}<\/code><\/pre>\n<p>Admittedly, code like the above is quite likely to raise some eyebrows during code review (when working with a team), but it just illustrates how we have to think about value types \u2014 such as structs and enums \u2014 when working with them. They don\u2019t have the same concept of <em>identity<\/em> and a <em>lifecycle<\/em>, like classes and actors do, which is important to remember when we design our types and their associated APIs.<\/p>\n<h2>Conclusion<\/h2>\n<p>So how <em>do<\/em> you decide between using <code>let<\/code> and <code>var<\/code> when declaring struct properties? My personal approach is to keep my struct properties <em>mutable by default<\/em>, since I feel like that really leans into the core concept of value types \u2014 that it\u2019s the enclosing value, not individual properties, that actually determines the real mutability of a given value.<\/p>\n<p>That being said, marking properties that we never expect to be mutated (such as a type\u2019s ID) as <code>let<\/code> is also usually a good practice \u2014 even though it doesn\u2019t strictly guarantee that such values will never change, it at least signals to everyone on the team what the <em>intended<\/em> mutability of such a property is.<\/p>\n<p>What do you think? Let me know what your thoughts are on this topic \u2014 along with any questions or feedback you might have \u2014 on either <a href=\"https:\/\/mastodon.social\/@johnsundell\">Mastodon<\/a> or <a href=\"https:\/\/bsky.app\/profile\/johnsundell.bsky.social\">Bluesky<\/a>.<\/p>\n<p>Thanks for reading!<\/p>","protected":false},"excerpt":{"rendered":"<p>When declaring a Swift property, we use either the let or var keyword depending on whether we want our new property to be read-only once assigned through the enclosing type\u2019s initializer, or whether we want to allow the property to be mutated and re-assigned multiple times. However, that\u2019s not the only difference between using let [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rop_custom_images_group":[],"rop_custom_messages_group":[],"rop_publish_now":"initial","rop_publish_now_accounts":[],"rop_publish_now_history":[],"rop_publish_now_status":"pending","footnotes":""},"categories":[1,15],"tags":[],"class_list":["post-9734","post","type-post","status-publish","format-standard","hentry","category-explore","category-world"],"_links":{"self":[{"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/posts\/9734","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/comments?post=9734"}],"version-history":[{"count":0,"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/posts\/9734\/revisions"}],"wp:attachment":[{"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/media?parent=9734"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/categories?post=9734"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/tags?post=9734"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}