Table of Contents
Issue
My issue AsValue
utility type feature request in TypeScript repository has been declined and Nov 2020. I expected the below API:
const MarkupPreprocessingDefaultSettings: AsValue = {
mustExecuteCodeQualityInspection: true,
waitingForOtherFilesWillBeSavedPeriod__milliseconds: 1000,
entryPointsGroupDependent: {
indentationSpacesCountInOutputCode: 2,
mustExecuteHTML5_Validation: true,
mustExecuteAccessibilityInspection: true
}
};
// Same as
const MarkupPreprocessingDefaultSettings: {
mustExecuteCodeQualityInspection: true,
waitingForOtherFilesWillBeSavedPeriod__milliseconds: 1000,
entryPointsGroupDependent: {
indentationSpacesCountInOutputCode: 2,
mustExecuteHTML5_Validation: true,
mustExecuteAccessibilityInspection: true
}
} = {
mustExecuteCodeQualityInspection: true,
waitingForOtherFilesWillBeSavedPeriod__milliseconds: 1000,
entryPointsGroupDependent: {
indentationSpacesCountInOutputCode: 2,
mustExecuteHTML5_Validation: true,
mustExecuteAccessibilityInspection: true
}
};
O’K, but what we have instead in reality?
Let’s consider the concrete problem.
Targets
Annotate the routing
variable such as:
- TypeScript must to know which properties are exists, and which are no. For example,
routing.products
must work, butrouting.gibberish
must cast the TypeScript error. - The count of keys in addition to
products
andorders
could be arbitrary large. - None of type annotating omitting,
any
andobject
allowed.
type Route = {
URN: string;
}
const routing /* : ??? */ = {
products: {
URN: "/products"
},
orders: {
URN: "/orders"
}
}
Option 1: Annotate everything manually (overkill)
const routing : {
products: Route;
orders: Route;
// and several thousands of keys for real commercial application ...
} = {
products: {
URN: "/products"
},
orders: {
URN: "/orders"
},
// and several thousands of keys for real commercial application ...
}
Conclusion: non-technological, hard to extend and maintenance. Unacceptable.
Option 2: Use indexed type (loose type saferty)
type Route = {
URN: string;
}
const Routing: { [routeName: string]: Route } = {
products: {
URN: "/products"
},
orders: {
URN: "/orders"
}
}
console.log(Routing.products)
console.log(Routing.orders)
console.log(Routing.gibberish) // Undefined!
Conclusion: it will not prevent the typo; no autocomplete available. Unacceptable.
Solution
Option 3: Use a generic helper function
const asRouting = <T extends Record<keyof T, Route>>(t: T) => t;
const routing = asRouting({
products: {
URN: "/products"
},
orders: {
URN: "/orders"
}
});
If you annotate the routing
variable at all, the compiler will treat the variable as being of the annotated type only, and you won’t get any type inference. As you see, either you have to annotate with exactly the right type, which is redundant, or you annotate a type which is wider than you want, and you lose type information you care about. So you really shouldn’t annotate routing
. Let the compiler infer its type for you.
But what if you mistakenly initialize routing
with a value where one or more of the properties is not a valid Route
? Well, the code that uses routing
will yield a compiler error. But that is likely to be quite far from the definition of routing
. You’d like the compiler error to be right there so you can fix it.
That’s where the generic helper function above comes in. asRouting()
is an identity function at runtime, and just outputs the same value as the input. At compile time, it is also an identity function, and the output type will be the same as the input type. But since this type, T
, is constrained to Record<keyof T, Route>
, asRouting()
will only accept inputs where all its properties are valid Route
objects.
Let’s see what happens with routing
as defined above. Its inferred type can be shown via IntelliSense to be:
/* const routing: {
products: {
URN: string;
};
orders: {
URN: string;
};
} */
Which is exactly the type you want, without requiring that you spell it out:
console.log(routing.products.URN); // okay
console.log(routing.orders.URN); // okay
console.log(routing.somethingElse); // compiler error!
// ---------------> ~~~~~~~~~~~~~
// Property 'somethingElse' does not exist
But you still get all the type checking you need to ensure that all properties are Route
:
const badRouting = asRouting({
products: {
URL: "/product"; // error!
// ~~~~~~~~~~~~~~~~
// Object literal may only specify known properties,
// and 'URL' does not exist in type 'Route'
}
})
Answered By – jcalz
Answer Checked By – Mary Flores (BugsFixing Volunteer)