Home > Net >  Is there a way to explicitly mutate the established control flow type of a variable in TypeScript?
Is there a way to explicitly mutate the established control flow type of a variable in TypeScript?

Time:11-17

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

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);

Playground

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;
  }
}

Playground

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

  • Related