deepkit / deepkit-framework

A new full-featured and high-performance TypeScript framework
https://deepkit.io/
MIT License
3.2k stars 123 forks source link

How is this possible? #258

Closed ben-pr-p closed 2 years ago

ben-pr-p commented 2 years ago

Hey team!

Blown away by this example from the docs:

import { is } from '@deepkit/type';

const user = await fetch('/user/1');
if (is<User>(user)) {
    user.username; //user object has been validated and its safe to assume its 'User'
}

is<number>(variable); //primitive types work, too

I took a look through the source of is, and I understand that this is accomplished by generating a serializer on the fly using new Function in @deepkit/core, but I still can't draw the link between how the type parameter passed to createTypeGuardFunction changes what function gets constructed.

I'd love a little addition to the docs there (happy to make that PR myself) - obviously your approach has great developer ergonomics and outstanding performance, and I think understanding that last bit of magic would help me better understand how I could extend it for my applications!

Great work though, super excited to see this mature!

marcj commented 2 years ago

hey ben, yeah it's a bit complicated at the beginning. Let me break it down for you:

  1. When you execute is<User> the bytecode of User is passed to the is function. The function is passes that data to getValidatorFunction.
  2. getValidatorFunction receives the bytecode again and constructs a full type object from that bytecode using a virtual machine. The resulting type object now contains the type kind (class in this case) with all property information.
  3. getValidatorFunction requests a cache container for this class type object and creates a new type guard function for exactly this type if not already done.
  4. createTypeGuardFunction builds a specialised function via new Function under the hood for the given type and returns that function. It uses the registered type guard templates of the passed serializer (which is per default the json/js serializer, which knows exactly how to type check all js types) and builds a function that checks the type of a passed data structure.
  5. The newly created function is cached and returned and directly executed in is.

Hope this helps. Feel free to join discord and ask what is still unclear, would love to help.

ben-pr-p commented 2 years ago

Everything makes sense except for the first step:

When you execute is the bytecode of User is passed to the is function. The function is passes that data to getValidatorFunction.

When I look at the definition here:

export function is<T>(data: any, serializerToUse: Serializer = serializer, errors: ValidationErrorItem[] = [], receiveType?: ReceiveType<T>): data is T {
    //`errors` is passed to `is` to trigger type validations as well
    return getValidatorFunction(serializerToUse, receiveType)(data, { errors }) as boolean;
}

I don't see any way that the bytecode of T is available.

I'm guessing that the magic is in this receiveType business:

export function getValidatorFunction<T>(serializerToUse: Serializer = serializer, receiveType?: ReceiveType<T>): Guard<T> {
    if (!receiveType) throw new NoTypeReceived();
    const type = resolveReceiveType(receiveType);
    const jit = getTypeJitContainer(type);
    if (jit.__is) {
        return jit.__is;
    }
    const fn = createTypeGuardFunction(type, undefined, serializerToUse) || (() => undefined);
    jit.__is = fn;
    return fn as Guard<T>;
}

and maybe buried in the reflection library, but I'm still struggling to comprehend how we cross the types -> runtime values chasm. When I call is, I'm still not passing a third parameter, and the provision of T can't force an actual third parameter value to appear at runtime, and so it's undefined, and so throw new NoTypeReceived() should always be thrown.

Am I reading that wrong?

marcj commented 2 years ago

Oh I see. That's the job of the type compiler. You see in the generated JS code how the function call is modified to include the type. In the upcoming book/new docs is more explained about that: http://deepkit-book.herokuapp.com/deepkit-book-english.html#_typeninformation_empfangen It's not finished yet, but might already be useful to understand that better.

ben-pr-p commented 2 years ago

Gotcha, I understand - that makes sense that this wouldn't be possible without extensions to typescript itself!

I'll play around with some toy compilations and look at the generated code, with and without the proper tsconfig.json.

Thanks! Excited to see how I can use this approach in my project.