Skip to content

Commit

Permalink
Add hand-written type-guard support with ts-auto-guard:custom
Browse files Browse the repository at this point in the history
  • Loading branch information
webstrand committed Oct 18, 2024
1 parent 04fb67c commit be4545c
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 26 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,3 +206,27 @@ export function isPerson(obj: unknown): obj is Person {
)
}
```

## Use Custom Type-Guard Instead of Generating

ts-auto-guard cannot generate type-guards for all typescript types automatically. For instance a validator for string template literals or branded types cannot be automatically genetrated. If you want to use a type which cannot be validated automatically you can use the annotation `/** @see {name} ts-auto-guard:type-guard */`, where `name` is a function exported by the current file:

```ts
// my-project/Person.ts

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

export function isPersonId(x: unknown): x is string {
return typeof x === "number";
// or look up the identifier in a cache or database
}

/** @see {isPerson} ts-auto-guard:type-guard */
export type Person = {
id: PersonId,
name: string
}
```
in this example, the generated `isPerson` type-guard will delegate to the hand-written `isPersonId` for checking the type of the `id` field.
67 changes: 41 additions & 26 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ function getReadonlyArrayType(type: Type): Type | undefined {
function getTypeGuardName(
child: Guardable,
options: IProcessOptions
): string | null {
): { kind: 'generated' | 'custom'; typeGuardName: string } | null {
const jsDocs = child.getJsDocs()
for (const doc of jsDocs) {
for (const line of doc.getInnerText().split('\n')) {
Expand All @@ -165,11 +165,14 @@ function getTypeGuardName(
.match(/@see\s+(?:{\s*(@link\s*)?(\w+)\s*}\s+)?ts-auto-guard:([^\s]*)/)
if (match !== null) {
const [, , typeGuardName, command] = match
if (command !== 'type-guard') {
reportError(`command ${command} is not supported!`)
return null
if (command === 'custom') {
return { kind: 'custom', typeGuardName }
}
if (command === 'type-guard') {
return { kind: 'generated', typeGuardName }
}
return typeGuardName
reportError(`command ${command} is not supported!`)
return null
}
}
}
Expand All @@ -181,7 +184,7 @@ function getTypeGuardName(
.filter(x => x && x.getName() !== '__type')[0]
?.getName()
if (name) {
return 'is' + name
return { kind: 'generated', typeGuardName: 'is' + name }
}
}
return null
Expand Down Expand Up @@ -1163,32 +1166,44 @@ export function processProject(
)

for (const typeDeclaration of allTypesDeclarations) {
const typeGuardName = getTypeGuardName(typeDeclaration, options)
if (typeGuardName !== null) {
records.push({ guardName: typeGuardName, typeDeclaration, outFile })
const rule = getTypeGuardName(typeDeclaration, options)
if (rule !== null) {
const { kind, typeGuardName } = rule
if (kind === 'custom') {
records.push({
guardName: typeGuardName,
typeDeclaration,
outFile: sourceFile,
})
} else if (kind === 'generated') {
records.push({ guardName: typeGuardName, typeDeclaration, outFile })
}
}
}

for (const typeDeclaration of allTypesDeclarations) {
const typeGuardName = getTypeGuardName(typeDeclaration, options)
if (typeGuardName !== null) {
functions.push(
generateTypeGuard(
typeGuardName,
typeDeclaration,
addDependency,
project,
records,
outFile,
options
const rule = getTypeGuardName(typeDeclaration, options)
if (rule !== null) {
const { kind, typeGuardName } = rule
if (kind === 'generated') {
functions.push(
generateTypeGuard(
typeGuardName,
typeDeclaration,
addDependency,
project,
records,
outFile,
options
)
)
)

addDependency(
sourceFile,
typeDeclaration.getName(),
typeDeclaration.isDefaultExport()
)
addDependency(
sourceFile,
typeDeclaration.getName(),
typeDeclaration.isDefaultExport()
)
}
}
}

Expand Down
36 changes: 36 additions & 0 deletions tests/features/imports_and_uses_custom_type_guard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { testProcessProject } from '../generate'

testProcessProject(
'imports and uses custom type guard',
{
'test.ts': `
/** @see {isFoo} ts-auto-guard:custom */
export type Foo = string & { brand: true };
export function isFoo(x: unknown): x is string {
return typeof x === "string";
}
/** @see {isBar} ts-auto-guard:type-guard */
export type Bar = {
foo: Foo,
str: string
}`,
},
{
'test.ts': null,
'test.guard.ts': `
import { isFoo, Bar } from "./test";
export function isBar(obj: unknown): obj is Bar {
const typedObj = obj as Bar
return (
(typedObj !== null &&
typeof typedObj === "object" ||
typeof typedObj === "function")&&
isFoo(typedObj["foo"]) as boolean &&
typeof typedObj["str"] === "string"
)
}`,
}
)

0 comments on commit be4545c

Please sign in to comment.