Add dependent type demos

master
Garrett Mills 2 years ago
parent 0b710eed40
commit 3c3132a7b3

1
.gitignore vendored

@ -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

@ -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 <shout@garrettmills.dev>",
"license": "MIT",

@ -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==}

@ -0,0 +1,44 @@
// ============ An implementation of first() ============
/**
* Get the type of the ith element of a tuple.
* Kind: Vector -> number -> *
*/
type ElementOf<Arr extends unknown[], Index extends keyof Arr> = 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 extends unknown[]> = Arr & { 0: ElementOf<Arr, 0> }
/**
* Gets the first element of a non-empty tuple.
*
* Accepts a parameter `arr` of type `NonEmpty<Arr>` where `Arr` is the inferred
* type of the tuple.
*
* @param arr
*/
function first<Arr extends unknown[]>(arr: NonEmpty<Arr>): ElementOf<Arr, 0> {
return arr[0]
}
const firstNumber = first([3.14]) // => type: number
const firstString = first(['hello', 3.14]) // => type: string
// const firstEmpty = first([]) // => type: never

@ -1,3 +0,0 @@
export const HELLO_WORLD = 'Hello, World!'

@ -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 boolean> = Success extends true
? {issuer: string, expires: Date}
: {error: string}
// Hover over the identifier to view the computed types.
type ExampleSuccess = CertCheckResponse<true>
type ExampleFail = CertCheckResponse<false>
/**
* 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<typeof success>
function sendCheckResponse(success: false, error: string): CertCheckResponse<typeof success>
function sendCheckResponse(success: boolean, issuerOrError: string, expires?: Date): CertCheckResponse<typeof success>
function sendCheckResponse(success: boolean, issuerOrError: string, expires: Date = new Date()): CertCheckResponse<typeof success> {
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?')

@ -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 extends unknown[], Item> = [...Arr, Item]
/** Converts a tuple of types and formats it as a function signature. */
type TypeArraySignature<Arr extends unknown[], Return> = (...params: Arr) => Return
type ExampleTypeArray = AppendTypeArray<['test', 3.14], true> // => ['test', 3.14, true]
type ExampleTypeSignature = TypeArraySignature<ExampleTypeArray, void> // => ('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<T> = (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<HandlerParams extends unknown[] = []> {
/** 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<unknown>[] = []
/** Register a new middleware parameter. */
public withParameter<T>(provider: ParameterProvider<T>): Route<AppendTypeArray<HandlerParams, T>> {
this.parameters.push(provider)
return this as unknown as Route<AppendTypeArray<HandlerParams, T>>
}
/** Register an appropriately-typed handler for this route. */
public handleWith(handler: TypeArraySignature<HandlerParams, unknown>): 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)*/
Loading…
Cancel
Save