Add dependent type demos
This commit is contained in:
parent
0b710eed40
commit
3c3132a7b3
1
.gitignore
vendored
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==}
|
||||
|
44
src/first.ts
Normal file
44
src/first.ts
Normal file
@ -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!'
|
||||
|
63
src/overloading.ts
Normal file
63
src/overloading.ts
Normal file
@ -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?')
|
80
src/routes.ts
Normal file
80
src/routes.ts
Normal file
@ -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…
Reference in New Issue
Block a user