Created
April 1, 2018 22:18
-
-
Save jack-williams/39aefc1145a60c3c42dbb9dc60a12e51 to your computer and use it in GitHub Desktop.
Catch TypeScript types at run-time using Proxies.
This file contains hidden or 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
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