Skip to main content

Migration from existing patterns

Replacing switch

You can turn a switch into either a match statement or a match expression, depending on its usage.

If you are using an IDE, you can use the “Refactor switch to match” refactoring code-action to do most of the below. To activate, the switch needs the following properties:

  • Every case of the switch must end with a break, return, or throw, except the last case.
  • If there is a default, it must be the last case.
  • The case test must be convertible to a match pattern.
  • If there is a let or const in the case body, it must be wrapped in a block.

And the caveats for the resulting match:

  • It may contain other breaks (other than the last one that was removed) that will be a parse error and you will have to figure out what to do with that.
  • It may not be exhaustively checked, or identifier patterns may not be valid (e.g. just typed as string). You will get new errors you will have to resolve.

To match statement

Most switch statements can be turned into match statements:

  • Replace switch with match
  • Delete the case
  • Replace the colon : after the case test with an arrow =>
  • Wrap the case body in a block { ... }
  • Remove the break;
  • If multiple cases share a body, use an "or" pattern |
  • Replace the default with a wildcard _
// Before
switch (action) {
case 'delete':
case 'remove':
data.pop();
break;
case 'add':
data.push(1);
break;
default:
show(data);
}

// After
match (action) {
'delete' | 'remove' => {
data.pop();
}
'add' => {
data.push(1);
}
_ => {
show(data);
}
}

If you are depending on the fallthrough behavior of switch cases when not using break (other than the simple case where the body is completely shared), then you will likely have to refactor your case body code into a function which is called.

To match expression

If every case of your switch has a body that contains a single return or a single assignment, then you can turn your switch into a match expression.

For the return case, make the same changes as in the to match statement section, except:

  • Replace each case body with only the expression being returned, and delete the return, and don't use braces for the case body
  • Separate each case with a comma ,
  • Return the entire match expression
1// Before2function getSizeBefore(imageSize: 'small' | 'medium' | 'large') {3  switch (imageSize) {4    case 'small':5      return 50;6    case 'medium':7      return 100;8    case 'large':9      return 200;10  };11}12
13// After14function getSizeAfter(imageSize: 'small' | 'medium' | 'large') {15  return match (imageSize) {16    'small' => 50,17    'medium' => 100,18    'large' => 200,19  };20}

For the assignment case, make the same changes as in the to match statement section, except:

  • Replace each case body with only the expression being assigned, and don't use braces for the case body
  • Separate each case with a comma ,
  • Assign the entire match expression to the variable
  • If you no longer re-assign the variable, you can change it to a const
// Before
let colorSchemeStyles;
switch (colorScheme) {
case 'darker':
colorSchemeStyles = colorSchemeDarker;
break;
case 'light':
colorSchemeStyles = colorSchemeLight;
break;
case 'unset':
colorSchemeStyles = colorSchemeDefault;
break;
}

// After
const colorSchemeStyles = match (colorScheme) {
'darker' => colorSchemeDarker,
'light' => colorSchemeLight,
'unset' => colorSchemeDefault,
};

Replacing conditional ternary expressions

You can replace most conditional expressions cond ? x : y with match expressions. This is particularly useful for complex or nested conditional expressions. For example:

1declare const obj:2  | {type: 'a', foo: number}3  | {type: 'b', bar: string}4  | null;5
6// Before7const a =8  obj === null9    ? 010    : obj.type === 'a'11      ? obj.foo12      : obj.bar.length;13
14// After15const b = match (obj) {16  {type: 'a', const foo} => foo,17  {type: 'b', const bar} => bar.length,18  null => {}19};

Dealing with disjoint object unions

Disjoint object unions are unions of object types with some distinguishing property. For example:

Previous patterns would involve checking the result.type property, and then accessing properties off of result:

1type Result = {type: 'ok', value: number} | {type: 'error', error: Error};2declare const result: Result;3
4switch (result.type) {5  case 'ok':6    console.log(result.value);7    break;8  case 'error':9    throw result.error;10}

With pattern matching you have to change how you think about it. Rather than doing the conditional checks on the type property, you do it on the object itself, and destructure what you need right in the pattern:

1type Result = {type: 'ok', value: number} | {type: 'error', error: Error};2declare const result: Result;3
4match (result) {5  {type: 'ok', const value} => {6    console.log(value);7  }8  {type: 'error', const error} => {9    throw error;10  }11}

If you need to pass in the refined object type itself, you can use an as pattern on the object pattern:

1type OK = {type: 'ok', value: number};2type Err = {type: 'error', error: Error}3type Result = OK | Err;4
5declare const result: Result;6
7match (result) {8  {type: 'ok', const value} => {9    console.log(value);10  }11  {type: 'error', ...} as err => {12    throw processError(err);13  }14}15
16declare function processError(err: Err): Error;