Created
February 11, 2021 00:51
-
-
Save JSuder-xx/e70dad6d9df937f600454ee580b1d06a to your computer and use it in GitHub Desktop.
An example of type system verified strings in TypeScript using template literal strings and tagged types.
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
/** | |
* Yet another demonstration of TypeScript awesomeness. | |
* | |
* Demonstrates tagged types and template literal strings (v4.1) which allows | |
* * for the type system to infer the literal string type of value | |
* * then deconstruct the literal string type into parts | |
* * in order to parse and validate strings directly in the type system. | |
* | |
* See the TRY comments for things to try. | |
* | |
* This example can be opened in the TS Playground (https://www.typescriptlang.org/play) | |
* or locally. It does require v4.1 or later. | |
*/ | |
module ReadMe {} | |
//-------------------------------------------------------------------------- | |
// APIs | |
//-------------------------------------------------------------------------- | |
type Tag<tag extends string> = { __tag: tag } | |
/** A string verified to be structured as an EMail. */ | |
module EmailString { | |
type EmailTag = Tag<"EMail"> | |
type EmailString = string & EmailTag | |
/** | |
* An EmailString | |
* - Assignable to string **BUT** string is not assignable to this. | |
* - Is a string with Something@SomethingElse. Essentially a string with an @ between two non-empty parts. | |
**/ | |
export type Type = EmailString; | |
type FromLiteral<s extends string> = | |
string extends s | |
? unknown // if s is general (unrefined string) then fail | |
: s extends `${infer _prefix}@${infer _suffix}` | |
? _prefix extends "" | |
? never | |
: _suffix extends "" | |
? never | |
: s & EmailTag | |
: unknown | |
/** Refines the type of the given string to EmailString if it has two non-empty strings separated by @. */ | |
export const literal = <str extends string>(str: str): FromLiteral<str> => str as any; | |
/** If you have a general string, test and refine whether it is an EMail string. */ | |
export const test = (str: string): str is Type => { | |
const [prefix, suffix] = (str || "").split("@"); | |
return !!prefix && !!suffix; | |
} | |
} | |
type EatPrefix<prefix extends string, s> = s extends `${prefix}${infer rest}` ? rest : false; | |
type EatDigit<s> = EatPrefix<"0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9", s>; | |
module USPhoneNumberString { | |
type EatLeftParen<s> = EatPrefix<"(", s>; | |
type EatRightParen<s> = EatPrefix<")", s>; | |
type EatThreeDigits<s> = EatDigit<EatDigit<EatDigit<s>>> | |
type EatFourDigits<s> = EatDigit<EatThreeDigits<s>> | |
type EatAreaCode<s> = EatRightParen<EatThreeDigits<EatLeftParen<s>>> | |
type EatPhoneNumber<s> = EatFourDigits<EatPrefix<"-", EatThreeDigits<s>>> | |
type EatPhoneNumberWithAreaCode<s> = EatPhoneNumber<EatAreaCode<s>> | |
type USPhoneNumberTag = Tag<"USPhoneNumber"> | |
type USPhoneNumberString = string & USPhoneNumberTag | |
/** | |
* A USPhoneNumberString | |
* - Assignable to string **BUT** string is not assignable to this. | |
* - Is a string that is either a seven digit or seven digit with area code. | |
**/ | |
export type Type = USPhoneNumberString | |
type FromLiteral<s extends string> = | |
EatPhoneNumber<s> extends string ? s & USPhoneNumberTag | |
: EatPhoneNumberWithAreaCode<s> extends string ? s & USPhoneNumberTag | |
: unknown; | |
/** Refines the type of the given string to PhoneNumberString if it looks like a US phone number.. */ | |
export const literal = <str extends string>(str: str): FromLiteral<str> => str as any; | |
/** If you have a general string, test and refine whether it is an EMail string. */ | |
export const test = (str: string): str is Type => { | |
const [prefix, suffix] = (str || "").split("@"); | |
return !!prefix && !!suffix; | |
} | |
} | |
type Person = { | |
firstName: string; | |
phoneNumber: USPhoneNumberString.Type; | |
email: EmailString.Type; | |
} | |
const poorlyFormed: Person = { | |
firstName: "Bob", | |
// TRY: Remove the line below to see errors | |
// @ts-expect-errors | |
phoneNumber: "123", | |
// TRY: Remove the line below to see errors | |
// @ts-expect-errors | |
email: "aol" | |
} | |
const validPerson: Person = { | |
firstName: "Janice", | |
// TRY: Remove a digit from the phone number | |
phoneNumber: USPhoneNumberString.literal("(123)123-4567"), | |
// TRY: Remove the @ symbol OR remove all the text to the left of the @. | |
email: EmailString.literal("[email protected]") | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment