Skip to content

Commit

Permalink
add branded type
Browse files Browse the repository at this point in the history
  • Loading branch information
andrej-dyck committed May 11, 2024
1 parent 611095e commit cb3cf21
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"pnpm": ">=9.1.0"
},
"devDependencies": {
"@type-challenges/utils": "0.1.1",
"@types/node": "20.12.11",
"eslint": "9.2.0",
"typescript": "5.4.5",
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

57 changes: 57 additions & 0 deletions types/Branded.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { Equal, Expect, NotEqual } from '@type-challenges/utils'
import { expect, test } from 'vitest'
import { raise } from '../raise/index.js'
import { Branded } from './Branded.ts'

/** branded numbers */
type Age = Branded<number, 'Age'>
type Year = Branded<number, 'Year'>

const validAge = (age: number): Age | undefined =>
age >= 0 && age <= 125 ? age as Age : undefined

const birthYear = (age: Age, now: Date): Year =>
now.getFullYear() - age as Year // it's incorrect, but sufficient for demo purpose

test('birthYear is compile-time safe', () => {
const fiveYears = validAge(5) ?? raise('invalid age')
expect(
birthYear(fiveYears, new Date(2024, 4, 11))
).toBe(2019)
})

test('birthYear is compile-time safe', () => {
expect(
// @ts-expect-error argument must be Age
birthYear(5, new Date(2024, 4, 11))
).toBe(2019)
})

const someAge = validAge(5) ?? raise('invalid age')
const someBirthYear = birthYear(someAge, new Date(2024, 4, 11))

// @ts-ignore unused

Check failure on line 33 in types/Branded.test.ts

View workflow job for this annotation

GitHub Actions / build

Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free

Check failure on line 33 in types/Branded.test.ts

View workflow job for this annotation

GitHub Actions / build

Use "@ts-expect-error" to ensure an error is actually being suppressed
type AgeTests = [

Check failure on line 34 in types/Branded.test.ts

View workflow job for this annotation

GitHub Actions / build

'AgeTests' is defined but never used
Expect<Equal<typeof someAge, Age>>,
Expect<NotEqual<typeof someAge, number>>,
Expect<Equal<Parameters<typeof validAge>, [number]>>,
Expect<Equal<Parameters<typeof birthYear>, [Age, Date]>>,
Expect<Equal<typeof someBirthYear, Year>>,
Expect<NotEqual<typeof someBirthYear, number>>,
]

/** branded string */
type Email = Branded<string, 'Email'>

const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/

Check failure on line 46 in types/Branded.test.ts

View workflow job for this annotation

GitHub Actions / build

Unnecessary escape character: \[

Check failure on line 46 in types/Branded.test.ts

View workflow job for this annotation

GitHub Actions / build

Unnecessary escape character: \[
const validEmail = (email: string): Email | undefined =>
emailRegex.test(email) ? email as Email : undefined

const someEmail = validEmail('[email protected]') ?? raise('invalid email')

// @ts-ignore unused

Check failure on line 52 in types/Branded.test.ts

View workflow job for this annotation

GitHub Actions / build

Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free

Check failure on line 52 in types/Branded.test.ts

View workflow job for this annotation

GitHub Actions / build

Use "@ts-expect-error" to ensure an error is actually being suppressed
type EmailTests = [

Check failure on line 53 in types/Branded.test.ts

View workflow job for this annotation

GitHub Actions / build

'EmailTests' is defined but never used
Expect<Equal<typeof someEmail, Email>>,
Expect<NotEqual<typeof someEmail, string>>,
Expect<Equal<Parameters<typeof validEmail>, [string]>>,
]
3 changes: 3 additions & 0 deletions types/Branded.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare const __brand: unique symbol
type Brand<B> = { [__brand]: B }
export type Branded<T, B> = T & Brand<B>
1 change: 1 addition & 0 deletions types/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './Branded.ts'
export * from './DeepPartial.ts'

0 comments on commit cb3cf21

Please sign in to comment.