diff --git a/package.json b/package.json index e2de296..d5de04c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c5b769..b482386 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: devDependencies: + '@type-challenges/utils': + specifier: 0.1.1 + version: 0.1.1 '@types/node': specifier: 20.12.11 version: 20.12.11 @@ -285,6 +288,9 @@ packages: '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + '@type-challenges/utils@0.1.1': + resolution: {integrity: sha512-A7ljYfBM+FLw+NDyuYvGBJiCEV9c0lPWEAdzfOAkb3JFqfLl0Iv/WhWMMARHiRKlmmiD1g8gz/507yVvHdQUYA==} + '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} @@ -1182,6 +1188,8 @@ snapshots: '@sinclair/typebox@0.27.8': {} + '@type-challenges/utils@0.1.1': {} + '@types/estree@1.0.5': {} '@types/json-schema@7.0.15': {} diff --git a/types/Branded.test.ts b/types/Branded.test.ts new file mode 100644 index 0000000..262051a --- /dev/null +++ b/types/Branded.test.ts @@ -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 +type Year = Branded + +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 +type AgeTests = [ + Expect>, + Expect>, + Expect, [number]>>, + Expect, [Age, Date]>>, + Expect>, + Expect>, +] + +/** branded string */ +type Email = Branded + +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,}))$/ +const validEmail = (email: string): Email | undefined => + emailRegex.test(email) ? email as Email : undefined + +const someEmail = validEmail('abc@mail.com') ?? raise('invalid email') + +// @ts-ignore unused +type EmailTests = [ + Expect>, + Expect>, + Expect, [string]>>, +] diff --git a/types/Branded.ts b/types/Branded.ts new file mode 100644 index 0000000..c992e14 --- /dev/null +++ b/types/Branded.ts @@ -0,0 +1,3 @@ +declare const __brand: unique symbol +type Brand = { [__brand]: B } +export type Branded = T & Brand diff --git a/types/index.ts b/types/index.ts index cd80594..efe43e2 100644 --- a/types/index.ts +++ b/types/index.ts @@ -1 +1,2 @@ +export * from './Branded.ts' export * from './DeepPartial.ts'