This page shows the source for this entry, with WebCore formatting language tags and attributes highlighted.

Title

C# vs. TypeScript type-narrowing and coercion

Description

I was working with a colleague to get the properties that have a particular attribute. The original formulation returned the properties then got the attributes <i>again</i>, plucking the first one off of the list and asserting that it exists to convince the compiler that everything's OK. We know it exists because otherwise we wouldn't have returned the property---but the computer doesn't know that. Ok, it works but it's not efficient or elegant. Is there some way to build this so we allocate minimally and don't have to use the null-forgiving ("dammit") operator? I proposed the following formulation. The null-forgiving operator bugs me a bit because I feel like TypeScript would have determined that <c>attribute</c> could no longer be <c>null</c>. C#/Roslyn doesn't do that. <code>private static IEnumerable<(PropertyInfo PropertyInfo, TAttribute Attribute)> GetPropertiesAndAttributes<tattribute>(Type type) { return from prop in type.GetProperties() let attribute = prop.GetCustomAttributes(typeof(TAttribute), false).FirstOrDefault() as TAttribute where attribute != null select (prop, attribute!); }</code> My collaborator prefers the non-query syntax for Linq, so he rewrote it as follows. <code>private static IEnumerable<(PropertyInfo PropertyInfo, TAttribute Attribute)> GetPropertiesAndAttributes<tattribute>(Type type) { return packetType .GetProperties() .Where(prop => prop.GetCustomAttributes(typeof(TAttribute), false).Length != 0) .Select(propInfo => (propInfo, propInfo.GetCustomAttribute<tattribute>()!)); }</code> I really don't like that it calls both <c>GetCustomAttributes()</c> and <c>GetCustomAttribute()</c>, so I looked into how to do emulate <c>let</c> with chained-method syntax. I found <a href="https://stackoverflow.com/questions/1092687/code-equivalent-to-the-let-keyword-in-chained-linq-extension-method-calls" source="StackOverflow">Code equivalent to the 'let' keyword in chained LINQ extension method calls</a> and rewrote the code as follows. <code>private static IEnumerable<(PropertyInfo PropertyInfo, TAttribute Attribute)> GetPropertiesAndAttributes<tattribute>(Type type) { return packetType .GetProperties() .Select(propInfo => (propInfo, attribute: propInfo.GetCustomAttributes(typeof(TAttribute), false).FirstOrDefault() as TAttribute)) .Where(t => t.attribute != null) .Select(t => (t.propInfo, t.attribute!)); }</code> I still can't get rid of the second <c>Select()</c> because the type of the first <c>Select()</c> is <code>(PropertyInfo PropertyInfo, TAttribute<hl>?</hl> Attribute)</code> rather than <code>(PropertyInfo PropertyInfo, TAttribute Attribute)</code> As in the other formulations, we still need the null-forgiving operator to coerce the type. In the final formulation, it's much clearer that this is only required for the compiler because the check that <c>attribute</c> is not <c>null</c> is made on the immediately preceding line. I was curious about TypeScript, though, C# only supports narrowing conversions for inbuilt primitives. Typescript is fancier. I used <a href="https://www.typescriptlang.org/play/">TypeScript Playground</a> for the examples below. Here's where we stand with C#, rewritten in TypeScript: <img src="{att_link}0_numbers_null_error.png" align="none"> At this point, TypeScript is making the same complaint as the C# compiler would. <img src="{att_link}1_numbers_null_error_message.png" align="none"> However, if you remove <c>null</c> values from the result, TypeScript recognizes that and automatically narrows the type to <c>number[]</c> from <c>(number | null)[]</c>. <img src="{att_link}2_numbers_converted.png" align="none"> I just realized that, while this example is interesting, I hadn't replicated the example from C# because I wasn't using tuples. So, let's try again and see how far TypeScript gets. <img src="{att_link}0_tuples_two_dimensional_array_default.png" align="none"> We already have a problem because TypeScript represents tuples with square brackets, which means that this could be an array of two-element tuples or a two-dimensional array. TypeScript defaults to the latter. We fix that with an explicit type. <img src="{att_link}1_tuples_correct_no_nulls.png" align="none"> If I assign <c>null</c> to <c>b</c> in one element, we have a problem, as expected. <img src="{att_link}2_tuples_null_element_is_invalid.png" align="none"> We fix that by adjusting the type of the local variable to be <c>[a: number, b: number | null]</c>. <img src="{att_link}3_tuples_return_type_conversion_error.png" align="none"> Now, we have the type-conversion error on the result. <img src="{att_link}4_tuples_return_type_conversion_error_message.png" align="none"> We fix that the same way as we do in C#, with a <c>Select</c>, which is called <c>map</c> in TypeScript/JavaScript. <img src="{att_link}5_tuples_coerced.png" align="none"> Since TypeScript doesn't allow you to directly address tuple elements like C# does, we have to "destructure" the elements with <c>const [a, b] = x)</c>. So, we have the same thing as in C#, where we allocate "new" tuples for the result. What we don't have is a <c>!</c> at the end because TypeScript recognizes the type-narrowing. Clever compiler. Of course, TypeScript <i>can</i> do this because it's just transpiling to JavaScript, which plays very fast and loose with types anyway. In C#, the compiler has to make decisions about the shape of the memory it uses, so a <c>Nullable<int></c> is going to have a different representation than an <c>int</c>. If you want to go from the former to the latter, then you have to define a conversion operator, either an <c>explicit</c> one or an <c>implicit</c> one. Or, as we did, you have to create a new tuple, which entails an allocation. Unfortunate, but unavoidable. In JavaScript, that's not the case, at least until it's run through a JIT, in which case other heuristics would have indicated how to most efficiently shape the storage for that particular instance. If that sentence doesn't make sense and you have a follow-up question, then me-from-ten-years-ago is here to offer more in the article <a href="https://www.earthli.com/news/view_article.php?id=3057">Optimizing compilation and execution for dynamic languages</a>, which summarizes a much-longer document written about the WebKit JavaScript engine. My colleague, who is always game to play with language features, had the good idea to try it with <c>filter()</c>. <img src="{att_link}2024-10-18_15_36_24-window.png" align="none"> This confirmed for us that the TypeScript checker doesn't actually know anything about the effects of the methods <c>filter</c> and <c>map</c>. It's just that the narrowing happens in the closure passed to the <c>map</c> version, so it can determine that there is no way that <c>b</c> will ever be <c>null</c> when the closure exits. Since it doesn't know the semantics of <c>filter</c>, it doesn't know that it actually does that too.