Skip to content

Instantly share code, notes, and snippets.

@JSuder-xx
Created February 11, 2021 00:51
Show Gist options
  • Save JSuder-xx/e70dad6d9df937f600454ee580b1d06a to your computer and use it in GitHub Desktop.
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.
/**
* 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