Skip to content

Close a support ticket

This example demonstrates how to close a support ticket in a ticketing system. The process includes finding the ticket by ID, validating its type and status, and then closing it with a specified reason. The code handles various error scenarios such as ticket not found, invalid ticket type, and invalid status transitions.

INFO

Note that this is a rather lengthy example, but it showcases the power of the typescript-result library in handling realistic workflows with multiple error types and validations.

ts
import { 
Result
} from "typescript-result";
enum
TicketStatus
{
Open
= "open",
Closed
= "closed",
} enum
TicketClosedReason
{
Resolved
= "resolved",
Duplicate
= "duplicate",
Other
= "other",
} enum
TicketType
{
Support
= "support",
Bug
= "bug",
Feature
= "feature",
} type
AnyTicket
=
SupportTicket
|
BugTicket
;
abstract class
Ticket
{
abstract
id
:
UUID
;
abstract
status
:
TicketStatus
;
abstract
type
:
TicketType
;
assertType
<
T
extends
TicketType
>(
type
:
T
) {
if (this.
type
!==
type
) {
return
Result
.
error
(
new
InvalidTicketTypeError
(
`Expected ticket type ${
type
}, but got ${this.
type
}`,
), ); } return
Result
.
ok
(this as unknown as
Extract
<
AnyTicket
, {
type
:
T
}>);
}
assertStatus
(
status
:
TicketStatus
) {
if (this.
status
!==
status
) {
return
Result
.
error
(
new
InvalidStatusTransitionError
(
`Expected ticket status ${
status
}, but got ${this.
status
}`,
), ); } return
Result
.
ok
(this);
} } class
SupportTicket
extends
Ticket
{
readonly
type
=
TicketType
.
Support
;
private constructor( public readonly
id
:
UUID
,
public
status
:
TicketStatus
,
public
closedReason
:
TicketClosedReason
| null,
public
closedReasonDescription
: string | null,
) { super(); }
close
(
reason
:
TicketClosedReason
,
description
?: string) {
return this.
assertStatus
(
TicketStatus
.
Open
).
map
(() => {
if (this.
closedReason
===
TicketClosedReason
.
Other
&& !
description
) {
return
Result
.
error
(
new
ValidationError
("Description is required for 'Other' reason"),
); } this.
status
=
TicketStatus
.
Closed
;
this.
closedReason
=
reason
;
this.
closedReasonDescription
=
description
?? null;
return
Result
.
ok
();
}); } static
create
() {
return new
SupportTicket
(
randomUUID
(),
TicketStatus
.
Open
, null, null);
} } class
BugTicket
extends
Ticket
{
readonly
type
=
TicketType
.
Bug
;
constructor( public readonly
id
:
UUID
,
public
status
:
TicketStatus
,
) { super(); } // Additional logic related to BugTicket here... } function
findTicketById
(
id
:
UUID
) {
return
Result
.
fromAsync
(async () => {
const
ticket
= await
fakeDatabaseLookup
(
id
);
if (!
ticket
) {
return
Result
.
error
(new
NotFoundError
(`Ticket with ID ${
id
} not found`));
} return
Result
.
ok
(
ticket
);
}); } function
closeTicket
(
ticketId
:
UUID
,
reason
:
TicketClosedReason
,
description
?: string,
) { return
findTicketById
(
ticketId
)
.
map
((
ticket
) =>
ticket
.
assertType
(
TicketType
.
Support
))
.
map
((
supportTicket
) =>
supportTicket
.
close
(
reason
,
description
));
} const
result
= await
closeTicket
("ticket-123",
TicketClosedReason
.
Resolved
);
ts
import { 
Result
} from "typescript-result";
enum
TicketStatus
{
Open
= "open",
Closed
= "closed",
} enum
TicketClosedReason
{
Resolved
= "resolved",
Duplicate
= "duplicate",
Other
= "other",
} enum
TicketType
{
Support
= "support",
Bug
= "bug",
Feature
= "feature",
} type
AnyTicket
=
SupportTicket
|
BugTicket
;
abstract class
Ticket
{
abstract
id
:
UUID
;
abstract
status
:
TicketStatus
;
abstract
type
:
TicketType
;
assertType
<
T
extends
TicketType
>(
type
:
T
) {
if (this.
type
!==
type
) {
return
Result
.
error
(
new
InvalidTicketTypeError
(
`Expected ticket type ${
type
}, but got ${this.
type
}`,
), ); } return
Result
.
ok
(this as unknown as
Extract
<
AnyTicket
, {
type
:
T
}>);
}
assertStatus
(
status
:
TicketStatus
) {
if (this.
status
!==
status
) {
return
Result
.
error
(
new
InvalidStatusTransitionError
(
`Expected ticket status ${
status
}, but got ${this.
status
}`,
), ); } return
Result
.
ok
(this);
} } class
SupportTicket
extends
Ticket
{
readonly
type
=
TicketType
.
Support
;
private constructor( public readonly
id
:
UUID
,
public
status
:
TicketStatus
,
public
closedReason
:
TicketClosedReason
| null,
public
closedReasonDescription
: string | null,
) { super(); } *
close
(
reason
:
TicketClosedReason
,
description
?: string) {
yield* this.
assertStatus
(
TicketStatus
.
Open
);
if (this.
closedReason
===
TicketClosedReason
.
Other
&& !
description
) {
return yield*
Result
.
error
(
new
ValidationError
("Description is required for 'Other' reason"),
); } this.
status
=
TicketStatus
.
Closed
;
this.
closedReason
=
reason
;
this.
closedReasonDescription
=
description
?? null;
} static
create
() {
return new
SupportTicket
(
randomUUID
(),
TicketStatus
.
Open
, null, null);
} } class
BugTicket
extends
Ticket
{
readonly
type
=
TicketType
.
Bug
;
constructor( public readonly
id
:
UUID
,
public
status
:
TicketStatus
,
) { super(); } // Additional logic related to BugTicket here... } function
findTicketById
(
id
:
UUID
) {
return
Result
.
fromAsync
(async () => {
const
ticket
= await
fakeDatabaseLookup
(
id
);
if (!
ticket
) {
return
Result
.
error
(new
NotFoundError
(`Ticket with ID ${
id
} not found`));
} return
Result
.
ok
(
ticket
);
}); } function
closeTicket
(
ticketId
:
UUID
,
reason
:
TicketClosedReason
,
description
?: string,
) { return
Result
.
gen
(async function* () {
const
ticket
= yield*
findTicketById
(
ticketId
);
const
supportTicket
= yield*
ticket
.
assertType
(
TicketType
.
Support
);
yield*
supportTicket
.
close
(
reason
,
description
);
}); } const
result
= await
closeTicket
("ticket-123",
TicketClosedReason
.
Resolved
);
ts
class 
NotFoundError
extends
Error
{
readonly
type
= "not-found-error";
} class
InvalidTicketTypeError
extends
Error
{
readonly
type
= "invalid-ticket-type-error";
} class
InvalidStatusTransitionError
extends
Error
{
readonly
type
= "invalid-status-transition-error";
} class
ValidationError
extends
Error
{
readonly
type
= "validation-error";
}