Skip to content

Instantly share code, notes, and snippets.

@jack-williams
Created April 1, 2018 22:18
Show Gist options
  • Save jack-williams/39aefc1145a60c3c42dbb9dc60a12e51 to your computer and use it in GitHub Desktop.
Save jack-williams/39aefc1145a60c3c42dbb9dc60a12e51 to your computer and use it in GitHub Desktop.
Catch TypeScript types at run-time using Proxies.
interface ObjectType {
kind: "object"
props: {[k: string]: Type[]};
}
interface FunctionType {
kind: "function";
calls: CallType[];
}
interface CallType {
args: Type[]
ret: Type
}
interface UnionType {
kind: "union"
types: Type[];
}
function makeFunctionType(): FunctionType {
return { kind: "function", calls: [] };
}
function makeObjectType(): ObjectType {
return { kind: "object", props: {} };
}
function makeCallType(): CallType {
return { args: [], ret: undefined };
}
function makeUnionType(types: Type[]): UnionType {
return { kind: "union", types };
}
type Type = "string" | "boolean" | "number" | "undefined" | FunctionType | ObjectType | UnionType;
function collectType<T>(v: T, register: (t: Type) => void): T {
if(typeof v === "function") {
return makeFunctionCollector(v,register);
}
if(typeof v === "object") {
return makeObjectCollector(v,register);
}
register(typeof v as any);
return v;
}
function makeFunctionCollector<T extends Function>(v: T, register: (t: Type) => void): T {
const funType = makeFunctionType();
register(funType);
return new Proxy(v,{
apply(target: T, thisArg: any, args: any[]) {
const callInfo = makeCallType();
funType.calls.push(callInfo);
const wrappedArgs = args.map(v => collectType(v,t => callInfo.args.push(t)));
const result = Reflect.apply(target,thisArg,wrappedArgs);
return collectType(result, t => callInfo.ret = t);
}
});
}
function makeObjectCollector<T extends Object>(v: T, register: (t: Type) => void): T {
const objType = makeObjectType();
register(objType);
return new Proxy(v,{
get(target: T, prop: string, receiver: any) {
return collectType(Reflect.get(target, prop, receiver), t => {
if(!objType.props[prop]) {
objType.props[prop] = []
}
objType.props[prop].push(t);
});
},
set(target: T, prop: string, receiver: any, val: any) {
return Reflect.set(target, prop, receiver, collectType(val, t => {
if(!objType.props[prop]) {
objType.props[prop] = []
}
objType.props[prop].push(t);
}));
}
});
}
function typeToString(t: Type): string {
if(typeof t !== "object") { return t; }
if(typeof t === "object" && t.kind === "function") {
return callsToString(t.calls);
}
if(typeof t === "object" && t.kind === "object") {
return objectToString(t);
}
return t.types.map(typeToString).join(" | ");
}
function typesToString(t: Type[]): string {
return typeToString(combineTypes(t));
}
function combineTypes(types: Type[]): Type {
const groundTypes = dedupGround(types.filter(t => typeof t !== "object") as any) as any;
const callFunTypes: FunctionType[] = types.filter(t => typeof t === "object" && t.kind === "function") as FunctionType[];
const objTypes: ObjectType[] = types.filter(t => typeof t === "object" && t.kind === "object") as ObjectType[];
const res = makeUnionType(groundTypes);
if(callFunTypes.length > 0) {
const fun = makeFunctionType();
fun.calls = [combineCalls(callFunTypes.reduce((prev,fn) => prev.concat(fn.calls),[]))];
res.types.push(fun);
}
if(objTypes.length > 0) {
res.types = res.types.concat(objTypes.map(t => squashObject(t)));
}
return res;
}
function squashObject(obj: ObjectType): ObjectType {
const newProps = {};
for(let k in obj.props) {
newProps[k] = [combineTypes(obj.props[k])];
}
const res = makeObjectType();
res.props = newProps;
return res;
}
function dedupGround(t: string[]): string[] {
const dict: Object = {};
for(let ty of t) {
dict[ty] = true;
}
return Object.getOwnPropertyNames(dict);
}
function combineCalls(calls: CallType[]): CallType {
const maxArg = calls.reduce((n,ct) => Math.max(n,ct.args.length), 0);
const combinedArgs: Type[] = [];
for(let i = 0; i < maxArg; i++) {
combinedArgs.push(combineTypes(calls.map(ct => ct.args[i] || "undefined")));
}
const combinedRet = combineTypes(calls.map(ct => ct.ret));
const callType = makeCallType();
callType.args = combinedArgs;
callType.ret = combinedRet;
return callType;
}
function callsToString(calls: CallType[]): string {
if(calls.length === 0) { return "any"; }
return calls.slice(1).reduce(
(prev,ct) => `${prev} & ` + callTypeToString(ct), callTypeToString(calls[0]));
}
function callTypeToString(call: CallType): string {
const argStr = call.args.map(typeToString).join(", ");
const ret = typeToString(call.ret);
return `(${argStr}) => ${ret}`;
}
function objectToString(obj: ObjectType): string {
const keyValStrings: string[] = []
for(let k in obj.props) {
keyValStrings.push(`${k} : ` + typeToString(obj.props[k][0] || undefined));
}
return "{ " + keyValStrings.join(', ') + " }";
}
const types: Type[] = [];
const collectTopLevel = (t: Type) => types.push(t);
function foo(f, x, y) {
return f(x.g,x.a) * (y ? -1 : 1);
}
let checkedFoo = collectType(foo,collectTopLevel);
let f = (g,x) => g(x*10);
checkedFoo(f, {g: x => x*2, a: 10}, true);
// foo has type: (((number) => number, number) => number, { g : (number) => number, a : number }, boolean) => number
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment