-
-
Notifications
You must be signed in to change notification settings - Fork 84
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add blog to website #132
Closed
Closed
Add blog to website #132
Changes from 3 commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { getPagesUnderRoute } from "nextra/context" | ||
import * as React from "react" | ||
import Link from "next/link" | ||
|
||
export declare namespace BlogIndex { | ||
export interface Props { | ||
readonly more?: string | ||
readonly pagesUnder: string | ||
} | ||
} | ||
|
||
export const BlogIndex: React.FC<BlogIndex.Props> = ({ | ||
more = "Read more", | ||
pagesUnder, | ||
}) => ( | ||
<React.Fragment> | ||
{getPagesUnderRoute(pagesUnder) | ||
.filter((page) => page.kind === "MdxPage") | ||
.map((page) => { | ||
const meta = "meta" in page ? page.meta : undefined | ||
const frontMatter = "frontMatter" in page ? page.frontMatter : undefined | ||
return ( | ||
<div key={page.route} className="mb-10"> | ||
<h3> | ||
<Link | ||
href={page.route} | ||
style={{ color: "inherit", textDecoration: "none" }} | ||
className="block font-semibold mt-8 text-2xl " | ||
> | ||
{frontMatter?.series && ( | ||
<span className="align-middle text-xs nx-bg-primary-100 nx-font-semibold nx-text-primary-800 dark:nx-bg-primary-100/10 dark:nx-text-primary-600 inline-block py-1 px-2 uppercase rounded mb-1 mr-2"> | ||
Series{" "} | ||
</span> | ||
)} | ||
<span>{meta?.title || frontMatter?.title || page.name}</span> | ||
</Link> | ||
</h3> | ||
<p className="opacity-80 mt-6 leading-7"> | ||
{frontMatter?.description}{" "} | ||
<span className="inline-block"> | ||
<Link | ||
href={page.route} | ||
className="text-[color:hsl(var(--nextra-primary-hue),100%,50%)] underline underline-offset-2 decoration-from-font" | ||
> | ||
{more + " →"} | ||
</Link> | ||
</span> | ||
</p> | ||
{frontMatter?.date ? ( | ||
<p className="opacity-50 text-sm mt-6 leading-7"> | ||
{frontMatter.date} | ||
</p> | ||
) : null} | ||
</div> | ||
) | ||
})} | ||
</React.Fragment> | ||
) |
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,5 +13,9 @@ | |
"theme": { | ||
"layout": "full" | ||
} | ||
}, | ||
"blog": { | ||
"title": "Blog", | ||
"type": "page" | ||
} | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
--- | ||
title: Blog | ||
searchable: false | ||
--- | ||
|
||
<h1 className="text-4xl tracking-tighter text-center font-extrabold md:text-5xl mt-8 pb-6"> | ||
Effect Blog | ||
</h1> | ||
|
||
import { BlogIndex } from "@/components/BlogIndex" | ||
|
||
<BlogIndex pagesUnder="/blog" /> |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"*": { | ||
"theme": { | ||
"pagination": false, | ||
"sidebar": false | ||
} | ||
}, | ||
"type-safe-errors": "Type-Safe Errors in TypeScript" | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
--- | ||
title: Blog | ||
description: 'In this article series we will explore a set of alternative options to represent errors in a type-safe way' | ||
searchable: false | ||
series: true | ||
--- | ||
|
||
import { BlogIndex } from "@/components/BlogIndex" | ||
|
||
<BlogIndex pagesUnder="/blog/type-safe-errors" /> |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"*": { | ||
"theme": { | ||
"pagination": true | ||
} | ||
}, | ||
"naive-approach": "A naive approach" | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
--- | ||
description: 'In this article we will take a look at the usual way we represent errors in TypeScript' | ||
date: June 7th, 2023 | ||
--- | ||
|
||
# A naive approach using classes with instanceof | ||
|
||
The first step to achieve type safety in error management as one may imagine is to define errors, let's have a look at a usual piece of code with the objective of improving its safety. | ||
|
||
```ts | ||
import * as S from "@effect/schema/Schema" | ||
|
||
const Todo_ = S.struct({ | ||
id: S.number, | ||
userId: S.number, | ||
title: S.string, | ||
completed: S.boolean, | ||
}) | ||
export interface Todo extends S.To<typeof Todo_> {} | ||
export const Todo: S.Schema<Todo> = Todo_ | ||
|
||
export const fetchTodo = (id: number) => | ||
fetch(`https://jsonplaceholder.typicode.com/todos/${id}`) | ||
.then((response) => response.json()) | ||
.then((value) => S.parseSync(Todo)(value)) | ||
``` | ||
|
||
Why can this code fail? when looking at the implementation we can easily spot at least 6 different causes of failure: | ||
|
||
1) network errors | ||
2) status not 200/404 | ||
3) malformed input, non integer | ||
4) todo can't be found (response 400) | ||
5) malformed response, non json | ||
6) malformed todo | ||
|
||
By looking at the type signature we have: | ||
|
||
```ts | ||
const fetchTodo: (id: number) => Promise<Todo> | ||
``` | ||
|
||
As we may imagine this isn't that helful in conveying the above information and we also have no way of really distinguishing between the failure cases unless we do some very deep investigation on what every single function we call can throw. | ||
|
||
The very first step would be to at least isolate the potential failure scenarios in a way that we can at least identify them separately. | ||
|
||
```ts | ||
class NetworkError { | ||
readonly _tag = "NetworkError" | ||
constructor(readonly error: unknown) {} | ||
} | ||
|
||
class ResponseStatusError { | ||
readonly _tag = "ResponseStatusError" | ||
constructor(readonly response: Response) {} | ||
} | ||
|
||
class MalformedResponse { | ||
readonly _tag = "MalformedResponse" | ||
constructor(readonly error: unknown) {} | ||
} | ||
|
||
class MalformedTodo { | ||
readonly _tag = "MalformedTodo" | ||
constructor(readonly error: unknown) {} | ||
} | ||
|
||
class MalformedId { | ||
readonly _tag = "MalformedId" | ||
constructor(readonly id: number) {} | ||
} | ||
|
||
class TodoNotFound { | ||
readonly _tag = "TodoNotFound" | ||
constructor(readonly id: number) {} | ||
} | ||
``` | ||
|
||
rewriting the code above and trying to use async-await leads to quick explosion, nesting try-catch blocks to identify every possible case looks horrible. We can get a decent result if we introduce a function like: | ||
|
||
```ts | ||
export const tryOrError = <T, E>( | ||
f: () => T, | ||
onErr: (error: unknown) => E | ||
): T => { | ||
try { | ||
const r = f() | ||
if (typeof r === "object" && r !== null && "then" in r) { | ||
// @ts-expect-error | ||
return (r as any as Promise<Awaited<T>>).catch((e) => { | ||
throw onErr(e) | ||
}) | ||
} | ||
return r | ||
} catch (e) { | ||
throw onErr(e) | ||
} | ||
} | ||
``` | ||
|
||
that wraps a generic function (or Promise-returning function) in a try-catch block isolating its error. | ||
|
||
Let's see how it would look like: | ||
|
||
|
||
```ts | ||
export const fetchTodo = async (id: number) => { | ||
if (!Number.isInteger(id)) { | ||
throw new MalformedId(id) | ||
} | ||
const response = await tryOrError( | ||
() => fetch(`https://jsonplaceholder.typicode.com/todos/${id}`), | ||
(error) => new NetworkError(error) | ||
) | ||
if (response.status === 404) { | ||
throw new TodoNotFound(id) | ||
} | ||
if (response.status !== 200) { | ||
throw new ResponseStatusError(response) | ||
} | ||
const json = await tryOrError( | ||
() => response.json(), | ||
(error) => new MalformedResponse(error) | ||
) | ||
return tryOrError( | ||
() => S.parseSync(Todo)(json), | ||
(error) => new MalformedTodo(error) | ||
) | ||
} | ||
``` | ||
|
||
While the type hasn't changed we are now able to isolate the individual causes of failure, for example we may write code like the following: | ||
|
||
```ts | ||
const main = async () => { | ||
try { | ||
const todos = await Promise.all([1, 2, 3, 4].map(fetchTodo)) | ||
console.log(`todos:`) | ||
for (const todo of todos) { | ||
console.log(`---`) | ||
console.log(`id: ${todo.id}`) | ||
console.log(`userId: ${todo.userId}`) | ||
console.log(`title: ${todo.title}`) | ||
console.log(`completed: ${todo.completed}`) | ||
console.log(`---`) | ||
} | ||
} catch (error) { | ||
if (error instanceof TodoNotFound) { | ||
console.error(`Todo with ${error.id} not found`) | ||
} else { | ||
console.error(`Unexpected error occurred`, error) | ||
} | ||
} | ||
} | ||
``` | ||
|
||
Here we are explicitely dealing with a Todo that was not found differently from the other scenarios, this is a classic example of a very important distinction. | ||
|
||
Not all errors are equal for our program, some errors are to be accounted for in our domain and some are not, like in this case a todo that was mistakenly retrieved by our consumer can be considered a predictable scenario to be hendled with a helpful message while a malformed response would instead indicate a defect of the system that should never occur in normal scenarios. | ||
|
||
Even if TS was smart enough to know all the error types that may occur in when doing `catch (error)` we would still be left with poor ability to disciminate between domain errors and defects and more importantly we can't have guarantees that our program won't fail for other reasons (such as a stack limit reached) so we effectively the type of `error` would always be `unknown | something else`, potentially with `something else` being a very long list of errors. | ||
|
||
Can we do better? yes, and that's what we will explore in the next article of the series where we will introduce the concept of result types. |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion:
Given that we explicitly recommend fully qualified name-spaced imports in the documentation, I would suggest being explicit in the blog as well. I.E: