rhys-vdw / ts-auto-guard

Generate type guard functions from TypeScript interfaces
MIT License
505 stars 54 forks source link

Add an annotation for hand-written type-guards `ts-auto-guard:custom` #314

Open webstrand opened 1 month ago

webstrand commented 1 month ago

ts-auto-guard can't generate correct type-guards for branded literal types:

/** @see {isPersonId} ts-auto-guard:type-guard */
export type PersonId = number & { brand: true };

// produces this:
export function isPersonId(obj: unknown): obj is PersonId {
    const typedObj = obj as PersonId
    return (
        typeof typedObj === "number" &&
        (typedObj !== null &&
            typeof typedObj === "object" ||
            typeof typedObj === "function") &&
        typedObj["brand"] === true
    )
}

Using this new annotation, auto-generation can be bypassed and a correct type-guard can be hand-written:

/** @see {isPersonId} ts-auto-guard:custom */
export type PersonId = number & { brand: true };
export function isPersonId(x: unknown): x is PersonId {
    return typeof x === "string"
}

Custom validators are useful for edge cases other than just branded literal types, where non-structural checks are desirable too. And it allows for integration with other type-guard generators like Zod.

I considered modifying the intersection logic to prune intersections between a literal and an object type, but that may quietly hide issues in the users types that would otherwise be surfaced by the type-guard rejecting all candidate values.

schmop commented 1 month ago

ts-auto-guard is a tool to generate type-guards automatically. Why exactly would you need an annotation to stop it doing that? Couldn't you just not annotate your type and typeguard, and then ts-auto-guard wouldn't override any self-written guards?

Edit: I think I found cases, where you want to use ts-auto-guard on custom types. When generating type-guards for more complex types, that should call the custom type guards. Example:

/** @see {isPersonId} ts-auto-guard:custom */
export type PersonId = number & { brand: true };
export function isPersonId(x: unknown): x is PersonId {
    return typeof x === "string"
}
/** @see {isPerson} ts-auto-guard:type-guard */
export type Person = {
    id: PersonId,
    name: string,
};

Here the typeguard isPerson for the type Person should call isPersonId. That would not happen without the annotations.

EditEdit: This is exactly the case, you put into the Readme in your commits. Reading the changeset explains the changeset. Sorry.

webstrand commented 1 month ago

Ah yeah I should have been clearer about that in the pull request. I need a way to tell ts-auto-guard to use my hand-written type-guard when automatically generating guards for other types.