I know we can use the as
keyword to inline assert types where they are needed. But what I'm trying to do here is to mutate the type of a variable in a code block such that the type persists until the block exits. I want to do this without creating separate variables and ideally without runtime assertions.
Summary of Requirements
- No
as
keyword type assertions - No creation of separate variable
- No runtime assertions
Example
Something I'm trying to write is a setProps
method that sets the properties of an object via a single function. The function is typed generically so that the right prop-to-value type is enforced. Inside the function is a large switch
statement that handles each property separately and each property may need access to the value multiple times (hence why I don't want to do as
assertions because it requires repetition).
Even more ideally I'd like TypeScript to infer the types of my values in the switch statement implicitly. But I don't think it's possible today.
Here is a simplified example of what I'm trying to achieve:
interface Props {
name: string;
age: number;
enrolled: boolean;
}
const props: Props = {
name: '',
age: 0,
enrolled: false,
};
export function setProp<K extends keyof Props>(prop: K, value: Props[K]): void {
const propName: keyof Props = prop; // This is needed because TypeScript can't break down the union within type K
switch (propName) {
case 'name':
props.name = value;
// ^-- Error!
// Type 'string | number | boolean' is not assignable to type 'string'.
// Type 'number' is not assignable to type 'string'.(2322)
break;
case 'age':
props.age = value;
// ^-- Same error!
break;
case 'enrolled':
props.enrolled = value;
// ^-- Same error!
break;
}
}
// Prop name and value type combination are enforced by TypeScript
setProp('name', 'John');
setProp('age', 20);
setProp('enrolled', true);
A close solution
The closest solution I came up with is to use a completely unchecked runtime assertion and rely on the tree shaking/dead-code elimination present in many bundlers today to remove them:
export function uncheckedAssert<T>(value: unknown): asserts value is T {
return;
}
The function can then be re-written as:
export function setProp<K extends keyof Props>(prop: K, value: Props[K]): void {
const propName: keyof Props = prop; // This is needed because TypeScript can't break down the union within type K
switch (propName) {
case 'name':
uncheckedAssert<Props[typeof propName]>(value);
props.name = value;
// ^-- No error!
break;
case 'age':
uncheckedAssert<Props[typeof propName]>(value);
props.age = value;
// ^-- No error!
break;
case 'enrolled':
uncheckedAssert<Props[typeof propName]>(value);
props.enrolled = value;
// ^-- No error!
break;
}
}
CodePudding user response:
As of TypeScript 4.9 there is no built-in type assertion operator which acts at the block scope level the way you want. See microsoft/TypeScript#10421 for the relevant feature request. The workarounds for this are pretty much those you already found.
The use case of being able to narrow some variable's apparent type this way is essentially covered by assertion functions. As mentioned in this comment on the pull request that introduced them:
It definitely would be nice to be able to explicitly assert a control flow type for a variable instead of having to cast on each reference. You can get the same effect with a call to an
asserts
function, but of course that call ends up in the emitted code. Still, JavaScript VMs are pretty decent at reducing away the cost of calls to empty functions.
Most of the requests for this are closed as duplicates of ms/TS#10421, with the note that assertion functions are the preferred solution. So I'd say it's unlikely that anything will change here in the near future. Still, anyone who's interested in seeing this might want to give that issue a