Created
August 15, 2018 20:08
-
-
Save alexsasharegan/1a681e8b79d731407eb67a74eab66937 to your computer and use it in GitHub Desktop.
A guide to pseudo pattern matching in TypeScript.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Step 1: | |
* Create a string enum. This can be done with a TS enum type, | |
* or a sum type (union of strings). | |
*/ | |
// enum style | |
enum TokenType { | |
Text = "Text", | |
UserMention = "UserMention", | |
ChannelMention = "ChannelMention", | |
} | |
// sum type style | |
type TokenType2 = "Text" | "UserMention" | "ChannelMention"; | |
/** | |
* Step 2: | |
* Define an object type that uses a discriminant (a common key), | |
* the type of which is your enum. | |
*/ | |
interface Token { | |
type: TokenType; | |
value: string; | |
} | |
/** | |
* Step 3: | |
* Create a matcher type. | |
* This will the object that behaves like our pseudo pattern matcher. | |
*/ | |
type TokenMatcher<T> = { [K in TokenType]: (value: string) => T }; | |
/** | |
* Step 4: | |
* Implement the match. | |
* We need our type with the discriminant from step 2 | |
* and the matcher from step 3 to do this. | |
* We also need a helper func, `expectNever`. | |
* The helper will guarantee we handle the enum exhaustively, | |
* or the compiler will error. This is accomplished with the `never` type. | |
* See the helper func for more info. | |
*/ | |
function matchTokenType<T>(tkn: Token, matcher: TokenMatcher<T>): T { | |
let { type, value } = tkn; | |
switch (type) { | |
case TokenType.Text: | |
return matcher.Text(value); | |
case TokenType.UserMention: | |
return matcher.UserMention(value); | |
case TokenType.ChannelMention: | |
return matcher.ChannelMention(value); | |
default: | |
return expectNever(type); | |
} | |
} | |
/** | |
* `never` is a type that can't exist. If we handle every possible case in our | |
* enum above, the default case will never match. | |
* | |
* An example of `never` would be the type of a variable assigned directly after | |
* throwing an error. Since the error is thrown, the assignment will never run. | |
* This type allows TS to signal things like unreachable code. | |
*/ | |
function expectNever(_: never, message = "Non exhaustive match."): never { | |
throw new Error(message); | |
} | |
/********************************** EXAMPLE ***********************************/ | |
let parsedSlackMessage: Token[] = [ | |
{ type: TokenType.Text, value: "Hello " }, | |
{ type: TokenType.UserMention, value: "John" }, | |
{ type: TokenType.Text, value: "! Welcome to the channel " }, | |
{ type: TokenType.ChannelMention, value: "general" }, | |
{ type: TokenType.Text, value: ". " }, | |
{ type: TokenType.UserMention, value: "channel" }, | |
{ type: TokenType.Text, value: ", say hello to our newest member!" }, | |
]; | |
// Convert the tokenized message to a string: | |
let stringifiedMessage = parsedSlackMessage.reduce( | |
(msg, tkn) => | |
msg + | |
matchTokenType(tkn, { | |
Text: s => s, | |
ChannelMention: s => "#" + s, | |
UserMention: s => "@" + s, | |
}), | |
"" | |
); | |
console.log(stringifiedMessage); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment