Awesome
TypeType ·
TypeType is designed to generate complex typescript type with ease.
- playground: https://mistlog.github.io/typetype-playground/
- introduction: The Art of Type Programming
- quick start: typetype template
Usage
> npm i -D @mistlog/typetype
CLI
example: typetype-examples/package.json
typetype build <dir>: build all *.type files in <dir>
typetype build -w <dir>: watch all *.type files in <dir>
typetype clean <dir>: remove all generated *.ts files in <dir>
typetype debug <file>: build <file> in debug mode(backtrace will be available)
API
example: typetype-examples/index.ts
import { transform } from "@mistlog/typetype";
const input = `
type function TypeName = (T) => ^{
if(T extends string) {
return "string"
} else {
return "number"
}
}
`;
const output = transform(input).code;
console.log(output);
// output: type TypeName<T> = T extends string ? "string" : "number";
Debug mode:
const output = transform(input, { debug: true }).code;
when debug
is true, backtrace will be available:
Expected end of input but ";" found.
x 1:11-1:11 MultiLineComment
| type a = 1;
| ^
o 1:11-1:11 _
| type a = 1;
| ^
x 1:11-1:11 TypeFunctionDeclaration
| type a = 1;
...
|/ /
| |
|/
o 1:1-1:11 TypeFile
type a = 1;
Examples
- all examples: https://github.com/mistlog/typetype-examples
In the url-parser example, function parseURL
will be translated to generic type parseURL<text>
in typescript:
// input
type function parseURL = (text) => ^{
if (parseProtocol<text> extends [infer protocol, infer rest]) {
return {
protocol,
rest
}
} else {
return never
}
}
// output
type parseURL<text> = parseProtocol<text> extends [infer protocol, infer rest]
? {
protocol: protocol;
rest: rest;
}
: never;
Conditional type is presented in this way:
^{ if ... else ...}
It can be nested so that the logic is clear:
type function _isNumberString = (text) => ^{
if(text extends "") {
return true
} else if(text extends `${infer digit}${infer rest}`) {
return ^{
if(digit extends Digit) {
return _isNumberString<rest>
} else {
return false
}
}
} else {
return false
}
}
- type query examples: Type Query: jQuery Style Type Manipulation
we can use js to create types:
type tuple = ["tesla", "model 3", "model X", "model Y"]
type result = ''' "use js"
return $.use("tuple")
.tupleToObject()
.omit(key => !key.startsWith("model"))
.type();
'''
generated:
type tuple = ["tesla", "model 3", "model X", "model Y"];
type result = {
"model 3": "model 3";
"model X": "model X";
"model Y": "model Y";
};
Syntax
- visit playground: https://mistlog.github.io/typetype-playground/
Basic type
type a = never
type b = number
type c = string
type value = 1
type bool = true
type tuple = [1, 2, 3]
type array = string[][]
type str = "abc"
type template = `value is: ${value}`
type obj = { a: 1, b: "abc", c: [1, 2] }
type valueDeep = obj["c"][1]
type keys = keyof { readonly a?: 1, b: 2 }
Union and Intersection
We use union [...]
or | [...]
to denote union type.
type u1 = union [0, 1, 2]
type u2 = | [0, 1, 2]
Because an intersection type combines multiple types into one, we use combine [...]
or & [...]
for intersection type:
type i1 = combine [{ a: 1 }, { b: 2 }]
type i2 = & [{ a: 1 }, { b: 2 }]
Function type
type f1 = type () => void
type f2 = type (a:number, b:string) => number
type f3 = type () => type (a:number, b:string) => void
Conditional type
/*
type conditional = 1 extends string ? "string" : "number"
*/
type conditional = ^{
if(1 extends string) {
return "string"
} else {
return "number"
}
}
nested:
/*
type conditional2 = 1 extends string ? "string" : 1 extends 1 ? "is 1" : "not 1";
*/
type conditional2 = ^{
if(1 extends string) {
return "string"
} else {
return ^{
if(1 extends 1) {
return "is 1"
} else {
return "not 1"
}
}
}
}
Mapped type
/* type mapped1 = { [K in Keys]: boolean } */
type mapped1 = ^{
for(K in Keys) {
return {
key: K,
value: boolean
}
}
}
/* type mapped2 = { [K in Keys as `get${K}`]: () => string } */
type mapped2 = ^{
for(K in Keys) {
return {
key: `get${K}`,
value: type () => string
}
}
}
Generic
/* export type Foo<T> = T extends { a: infer U; b: infer U; } ? U : never */
type function Foo = (T) => ^{
if(T extends {a: infer U, b: infer U}) {
return U
} else {
return never
}
}
With constraint:
/* export type MyPick<T, Keys extends keyof T> = { [K in Keys]: T[K] } */
export type function MyPick = (T, Keys extends keyof T) => ^{
for(K in Keys) {
return {
key: K,
value: T[K]
}
}
}
Object spread
Object spread syntax can be used, and it will be translated to object$assign<{}, [...]>
:
export type function parseURL = (text) => ^{
if (parseProtocol<text> extends [infer protocol, infer rest]) {
return {
protocol,
...parseAuthority<rest>
}
} else {
return never
}
}
as long as object$assign
is available globally, this works fine:
export type parseURL<text> = parseProtocol<text> extends [infer protocol, infer rest] ? object$assign<{}, [{
protocol: protocol;
}, parseAuthority<rest>]> : never;
you can polyfill it using type lib such as ts-toolbelt, for example: polyfill/global.d.ts.
How it works?
It's AST -> AST
transformation.
We use react-peg to write parser, as you can see in ./src/parser/expression, generator is even simpler than parser, in ./src/generator/generator, typetype AST
is used to generate corresponding babel AST
.
License
This project is MIT licensed.