generated from garrettmills/template-npm-typescript
parent
0b710eed40
commit
3c3132a7b3
@ -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…
Reference in new issue