From 3c3132a7b30c924db423c95edc946ae5b7054f23 Mon Sep 17 00:00:00 2001 From: garrettmills Date: Mon, 19 Sep 2022 10:25:40 -0500 Subject: [PATCH] Add dependent type demos --- .gitignore | 1 + package.json | 6 ++-- pnpm-lock.yaml | 17 +++++----- src/first.ts | 44 +++++++++++++++++++++++++ src/index.ts | 3 -- src/overloading.ts | 63 ++++++++++++++++++++++++++++++++++++ src/routes.ts | 80 ++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 199 insertions(+), 15 deletions(-) create mode 100644 src/first.ts delete mode 100644 src/index.ts create mode 100644 src/overloading.ts create mode 100644 src/routes.ts diff --git a/.gitignore b/.gitignore index 2840a2c..17cec40 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff +.idea .idea/**/workspace.xml .idea/**/tasks.xml .idea/**/usage.statistics.xml diff --git a/package.json b/package.json index 8beff3f..00f5582 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "template-npm-typescript", + "name": "garrettmills/typescript-dependent-types", "version": "0.1.0", - "description": "A template for NPM packages built with TypeScript", + "description": "Some small examples of dependent types in TypeScript", "main": "lib/index.js", "types": "lib/index.d.ts", "directories": { @@ -21,7 +21,7 @@ "postversion": "git push && git push --tags", "repository": { "type": "git", - "url": "https://code.garrettmills.dev/garrettmills/template-npm-typescript" + "url": "https://code.garrettmills.dev/garrettmills/typescript-dependent-types" }, "author": "Garrett Mills ", "license": "MIT", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a104601..e4c1c65 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: 5.3 +lockfileVersion: 5.4 specifiers: '@types/rimraf': ^3.0.2 @@ -24,8 +24,8 @@ dependencies: uuid: 8.3.2 devDependencies: - '@typescript-eslint/eslint-plugin': 5.4.0_d6f2571581882eb2d6c9d9867e002185 - '@typescript-eslint/parser': 5.4.0_eslint@8.2.0+typescript@4.5.2 + '@typescript-eslint/eslint-plugin': 5.4.0_23zfofmbraxlfvwj3gdh4abbqu + '@typescript-eslint/parser': 5.4.0_ple4j2pjmrfd4fppfabsrwszv4 eslint: 8.2.0 packages: @@ -141,7 +141,7 @@ packages: resolution: {integrity: sha512-0LbEEx1zxrYB3pgpd1M5lEhLcXjKJnYghvhTRgaBeUivLHMDM1TzF3IJ6hXU2+8uA4Xz+5BA63mtZo5DjVT8iA==} dev: false - /@typescript-eslint/eslint-plugin/5.4.0_d6f2571581882eb2d6c9d9867e002185: + /@typescript-eslint/eslint-plugin/5.4.0_23zfofmbraxlfvwj3gdh4abbqu: resolution: {integrity: sha512-9/yPSBlwzsetCsGEn9j24D8vGQgJkOTr4oMLas/w886ZtzKIs1iyoqFrwsX2fqYEeUwsdBpC21gcjRGo57u0eg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -152,8 +152,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/experimental-utils': 5.4.0_eslint@8.2.0+typescript@4.5.2 - '@typescript-eslint/parser': 5.4.0_eslint@8.2.0+typescript@4.5.2 + '@typescript-eslint/experimental-utils': 5.4.0_ple4j2pjmrfd4fppfabsrwszv4 + '@typescript-eslint/parser': 5.4.0_ple4j2pjmrfd4fppfabsrwszv4 '@typescript-eslint/scope-manager': 5.4.0 debug: 4.3.2 eslint: 8.2.0 @@ -167,7 +167,7 @@ packages: - supports-color dev: true - /@typescript-eslint/experimental-utils/5.4.0_eslint@8.2.0+typescript@4.5.2: + /@typescript-eslint/experimental-utils/5.4.0_ple4j2pjmrfd4fppfabsrwszv4: resolution: {integrity: sha512-Nz2JDIQUdmIGd6p33A+naQmwfkU5KVTLb/5lTk+tLVTDacZKoGQisj8UCxk7onJcrgjIvr8xWqkYI+DbI3TfXg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -185,7 +185,7 @@ packages: - typescript dev: true - /@typescript-eslint/parser/5.4.0_eslint@8.2.0+typescript@4.5.2: + /@typescript-eslint/parser/5.4.0_ple4j2pjmrfd4fppfabsrwszv4: resolution: {integrity: sha512-JoB41EmxiYpaEsRwpZEYAJ9XQURPFer8hpkIW9GiaspVLX8oqbqNM8P4EP8HOZg96yaALiLEVWllA2E8vwsIKw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -969,7 +969,6 @@ packages: resolution: {integrity: sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==} engines: {node: '>=4.2.0'} hasBin: true - dev: false /uri-js/4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} diff --git a/src/first.ts b/src/first.ts new file mode 100644 index 0000000..4400c13 --- /dev/null +++ b/src/first.ts @@ -0,0 +1,44 @@ + +// ============ An implementation of first() ============ + +/** + * Get the type of the ith element of a tuple. + * Kind: Vector -> number -> * + */ +type ElementOf = Arr[Index] + +// An example - this returns the type of the second element of the tuple +// In this case, literal values are types (kinda), so this resolves to the literal type 'hello' +// That is, it is satisfiable by the subset of strings {hello} +// In an editor like VSCode, hover over the name of the type to see what it resolved to +type ExampleElementOf = ElementOf<[true, 'hello', 3.14], 1> + +// This succeeds because the value 'hello' is assignable to the type 'hello' +const exampleElementGood: ExampleElementOf = 'hello' + +// This fails because the value 'fubar' is not assignable to the type 'hello' +// const exampleElementBad: ExampleElementOf = 'fubar' + +/** + * Accepts non-empty tuples. + * Does this by coercing the inferred tuple Arr to the intersection type Arr with an interface + * that MUST have an index 0 which has the type of the first element of Arr. + * Kind: Vector -> * + */ +type NonEmpty = Arr & { 0: ElementOf } + +/** + * Gets the first element of a non-empty tuple. + * + * Accepts a parameter `arr` of type `NonEmpty` where `Arr` is the inferred + * type of the tuple. + * + * @param arr + */ +function first(arr: NonEmpty): ElementOf { + return arr[0] +} + +const firstNumber = first([3.14]) // => type: number +const firstString = first(['hello', 3.14]) // => type: string +// const firstEmpty = first([]) // => type: never diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index acc5e3b..0000000 --- a/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ - -export const HELLO_WORLD = 'Hello, World!' - diff --git a/src/overloading.ts b/src/overloading.ts new file mode 100644 index 0000000..b73f71b --- /dev/null +++ b/src/overloading.ts @@ -0,0 +1,63 @@ + +// ============ An example using dependent types to overload function signatures ============ +// In this example, we define a type CertCheckResponse which defines the structure of a response +// object for a hypothetical certificate validation API. +// We then implement a function sendCheckResponse whose return type and parameter signature +// depends on the value of the success parameter. + +/** A helper function that gets a Date one hour in the future. */ +function inOneHour(): Date { + const d = new Date() + d.setMinutes(d.getMinutes() + 60) + return d +} + + +/** A type conditional on a boolean parameter. */ +type CertCheckResponse = Success extends true + ? {issuer: string, expires: Date} + : {error: string} + + +// Hover over the identifier to view the computed types. +type ExampleSuccess = CertCheckResponse +type ExampleFail = CertCheckResponse + + +/** + * This is where the magic happens. We combine TypeScripts function/method overloading with + * the dependent types to make not only the return type but also the function signature dependent + * on the value of the `success` parameter. + * + * (The third and final signature is the signature of the underlying implementation and must be + * compatible with all the other signatures.) + * + * @param success + * @param issuer + * @param expires + */ +function sendCheckResponse(success: true, issuer: string, expires: Date): CertCheckResponse +function sendCheckResponse(success: false, error: string): CertCheckResponse +function sendCheckResponse(success: boolean, issuerOrError: string, expires?: Date): CertCheckResponse +function sendCheckResponse(success: boolean, issuerOrError: string, expires: Date = new Date()): CertCheckResponse { + if ( success ) { + return { + issuer: issuerOrError, + expires: expires, + } + } + + return {error: issuerOrError} +} + + +const successResponse = sendCheckResponse(true, 'contoso.com', inOneHour()) // => {issuer: string, expires: Date} +const failureResponse = sendCheckResponse(false, 'Invalid issuer.') // => {error: string} + +// Interestingly, this gracefully degrades to more general types when the value +// of success cannot be inferred statically. This is why we include the 3rd overload +// signature with `success: boolean`. +let someBool!: boolean // example of a non-specific boolean value + +// This has the union type {issuer: string, expires: Date} | {error: string} +const ambiguousResponse = sendCheckResponse(someBool, 'String?') diff --git a/src/routes.ts b/src/routes.ts new file mode 100644 index 0000000..200a22a --- /dev/null +++ b/src/routes.ts @@ -0,0 +1,80 @@ + +// ============ An example using dependent/constructed types to build route handlers ============ +// This is a more advanced example that prevents a hypothetical web route definition library +// from accepting handler functions whose parameters do not match the types of the provided parameters. +// This is a simplification of real-world routing implementation I built in @extollo/lib: +// https://code.garrettmills.dev/extollo/lib/src/branch/master/src/http/routing/Route.ts +// https://code.garrettmills.dev/extollo/lib/src/branch/master/src/util/support/types.ts + +// First, we'll define some helper types for constructing/destructing variadic tuple types: + +/** Takes a tuple and a type and produces a new tuple suffixed w/ the type. */ +type AppendTypeArray = [...Arr, Item] + +/** Converts a tuple of types and formats it as a function signature. */ +type TypeArraySignature = (...params: Arr) => Return + +type ExampleTypeArray = AppendTypeArray<['test', 3.14], true> // => ['test', 3.14, true] +type ExampleTypeSignature = TypeArraySignature // => ('test', 3.14, true) => void + + +// Now, some helper types used by the Route class: +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface Request {} // Stub type for the HTTP request + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface HandledRoute {} // Prevents adding parameters to handled routes + +/** Type of functions that provide parameters to the request handler. */ +type ParameterProvider = (request: Request) => T + +/** + * Stub route definition implementation. The type of the handler function + * accepted by `handleWith(...)` depends on the parameter middleware added + * via `withParameter(...)`. + */ +export class Route { + + /** Start a net GET route. */ + public static get(endpoint: string): Route<[]> { + return new Route(endpoint) + } + + constructor( + public readonly endpoint: string, + ) {} + + /** The collected parameter middleware. */ + protected parameters: ParameterProvider[] = [] + + /** Register a new middleware parameter. */ + public withParameter(provider: ParameterProvider): Route> { + this.parameters.push(provider) + return this as unknown as Route> + } + + /** Register an appropriately-typed handler for this route. */ + public handleWith(handler: TypeArraySignature): HandledRoute { + return this + } +} + +Route.get('/no-param') + .handleWith(() => 'hello!') + +Route.get('/one-param') + .withParameter(() => 3.14) + .handleWith((pi: number) => `Pi is ${pi}!`) + +Route.get('/two-params') + .withParameter(() => 'Hello, ') + .withParameter(() => 'John Doe') + .handleWith((greeting: string, name: string) => `${greeting}${name}`) + +/* Route.get('/invalid-params') + .withParameter(() => 3.14) + .handleWith((pi: number, greeting: string) => `${pi}${greeting}`)*/ + +/* Route.get('/invalid-method-call') + .handleWith(() => 'Hello!') + .withParameter(() => 3.14)*/