Advanced TypeScript #4: loops in types
Welcome to the 100th (yay! 🎉) Edition of the Code with Hugo newsletter.
Here’s part 4 of 7 of the Advanced TypeScript series, where I share TypeScript patterns I’ve encountered in enterprise grade applications.
We're looking at a very interesting pattern that I wasn't aware of until years into my TypeScript journey, where we'll run loops in the TypeScript type system.
4. Zipping a type to a union
TS has a sort of loop construct using NonNullable<{ [K in keyof Fields]: /* use K here */ }>[keyof FIELDS]
. Which is useful for example if you want key-value pairwise types.
import { expectTypeOf } from 'expect-type';
interface Fields {
amount: number;
formattedAmount: string;
}
type FieldDefs = NonNullable<{
[K in keyof Fields]: {
name: K;
value: Fields[K];
};
}>[keyof Fields];
expectTypeOf<FieldDefs>().toEqualTypeOf<
{ name: 'amount'; value: number } | { name: 'formattedAmount'; value: string }
>();
Why is this useful? It keeps the type stricter, { name: 'amount', value: number }
ensures the name
value matches the value
type.
A more naive implementation doesn't have this strictness on the union:
type FieldNames = keyof Fields;
type FieldValues = Fields[keyof Fields];
expectTypeOf<FieldNames>().toEqualTypeOf<'amount' | 'formattedAmount'>();
expectTypeOf<FieldValues>().toEqualTypeOf<number | string>();
Note the lack of relationship between amount <-> number and formattedAmount <-> string, this means the type is not as strict as it could be.
function setFieldNaive(name: FieldNames, value: FieldValues) {}
setFieldNaive('amount', '200'); // no error
function setFieldStrict(update: FieldDefs) {}
// @ts-expect-error
// Argument of type '{ name: "amount"; value: string; }' is not assignable to parameter of type 'FieldDefs'.
// Types of property 'value' are incompatible.
// Type 'string' is not assignable to type 'number'.
setFieldStrict({ name: 'amount', value: '200' });
// no error
setFieldStrict({ name: 'amount', value: 200 });
That's this week's pattern, you can get a sneak peek of the rest at codewithhugo.com/typescript-types-in-the-trenches/, or access the annotated source at github.com/HugoDF/real-world-ts.