Skip to content

Commit

Permalink
feat: Typescript support (#24)
Browse files Browse the repository at this point in the history
* feat: add types declaration file for TS support

* test: add TS tests

* docs: add TS support documentation

* Update package.json

Co-authored-by: Manuel Spigolon <behemoth89@gmail.com>

* handle additional options

* chore: lint fix ts

* chore: lint fix ts

---------

Co-authored-by: Manuel Spigolon <behemoth89@gmail.com>
  • Loading branch information
andersonjoseph and Eomm authored Jul 25, 2023
1 parent 15a74b5 commit a535a07
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 2 deletions.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,31 @@ myForm.body // Stream of the string in application/x-www-form-urlencoded format
myForm.head // JSON with the `content-type` field set
```

## Typescript

This module ships with a handwritten TypeScript declaration file for TS support. The declaration exports a single function.

```ts
import formAutoContent from 'form-auto-content';
```

When an options object is provided, the result types will be accurately inferred:

```ts
import formAutoContent from 'form-auto-content';

const option = { payload: 'body', headers: 'head' } as const

const myCustomForm = formAutoContent({
field1: 'value1',
field2: ['value2']
}, option);

myCustomForm.body // ok
myCustomForm.head // ok

myCustomForm.payload // Typescript error: property 'payload' does not exists in type...
```

## License

Expand Down
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
"version": "3.1.0",
"description": "Build a form without headache",
"main": "index.js",
"types": "./types/index.d.ts",
"scripts": {
"lint": "standard",
"lint:fix": "standard --fix",
"test": "tap test/**/*.test.js"
"test": "tap test/**/*.test.js && tsd",
"test:typescript": "tsd"
},
"repository": {
"type": "git",
Expand All @@ -33,11 +35,14 @@
"homepage": "https://github.com/Eomm/form-auto-content#readme",
"devDependencies": {
"@fastify/multipart": "^7.6.0",
"@types/node": "^20.4.1",
"fastify": "^4.17.0",
"light-my-request": "^5.0.0",
"multiparty": "^4.2.1",
"standard": "^17.0.0",
"tap": "^16.3.0"
"tap": "^16.3.0",
"tsd": "^0.28.1",
"typescript": "^5.1.6"
},
"dependencies": {
"fast-querystring": "^1.0.0",
Expand Down
41 changes: 41 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Readable } from "stream";

export type FormMethodOptions = {
readonly payload?: string,
readonly headers?: string
}

type FormMethodDefaultOptions = {
readonly payload: 'payload',
readonly headers: 'headers'
}

type Neverify<T> = {
[K in keyof T]: never
}

type WithoutExtraProperties<BaseType, Arg extends BaseType> = Arg & Neverify<Omit<Arg, keyof BaseType>>

type ComputedFormProperty<T extends FormMethodOptions, Property extends keyof T, DefaultValue extends string, ReturnType> = {
[K in T[Property] as keyof T[Property] extends never
? DefaultValue
: K extends PropertyKey
? K
: never
]: ReturnType
}

type FormMethodResult<T extends FormMethodOptions, K extends keyof T = keyof T> = string extends T[K]
? ComputedFormProperty<T, 'headers', string, Record<string, string> | undefined> | ComputedFormProperty<T, 'payload', string, Readable | undefined>
: ComputedFormProperty<T, 'headers', 'headers', Record<string, string>> & ComputedFormProperty<T, 'payload', 'payload', Readable>

/**
* @param {Record<string, unknown>} json - A JSON object that defines the fields of the form.
* @param {FormMethodOptions} opts - An object containing properties to modify the output field names.
* @returns {FormMethodResult} A JSON object with a payload field representing the data stream and a headers field containing the content-type set to "application/json".
*/
export default function formMethod<T extends FormMethodOptions = FormMethodDefaultOptions>(
json: Record<string, unknown>,
opts?: T & WithoutExtraProperties<FormMethodOptions, T>
): FormMethodResult<T>

124 changes: 124 additions & 0 deletions types/index.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { expectAssignable } from 'tsd';
import formAutoContent, { FormMethodOptions } from ".";
import { Readable } from "stream";

{ // no options supplied
const myForm = formAutoContent({
field1: 'value1',
field2: ['value2', 'value2.2']
})

expectAssignable<{ payload: Readable, headers: Record<string, string> }>(myForm)
}

{ // object options with type FormMethodOptions specified
const option: FormMethodOptions = { payload: 'body', headers: 'head' } as const
const myForm = formAutoContent({
field1: 'value1',
field2: ['value2', 'value2.2']
}, option);

expectAssignable<{ [x: string]: Record<string, string> | Readable | undefined }>(myForm)
}

{ // object options with satysfing FormMethodOptions
const option = { payload: 'body', headers: 'head' } as const satisfies FormMethodOptions;

const myForm = formAutoContent({
field1: 'value1',
field2: ['value2', 'value2.2']
}, option);

expectAssignable<{ body: Readable, head: Record<string, string> }>(myForm)
}

{ // object options as const
const option = { payload: 'body', headers: 'head' } as const

const myForm = formAutoContent({
field1: 'value1',
field2: ['value2', 'value2.2']
}, option)

expectAssignable<{ body: Readable, head: Record<string, string> }>(myForm)
}

{ // object options as const and only payload defined
const option = { payload: 'body' } as const

const myForm = formAutoContent({
field1: 'value1',
field2: ['value2', 'value2.2']
}, option)

expectAssignable<{ body: Readable, headers: Record<string, string> }>(myForm)
}

{ // object options as const and only headers defined
const option = { headers: 'head' } as const

const myForm = formAutoContent({
field1: 'value1',
field2: ['value2', 'value2.2']
}, option)

expectAssignable<{ payload: Readable, head: Record<string, string> }>(myForm)
}

{ // object options without as const
const option = { payload: 'body', headers: 'head' }

const myForm = formAutoContent({
field1: 'value1',
field2: ['value2', 'value2.2']
}, option)

expectAssignable<{ [x: string]: Readable | Record<string, string> | undefined }>(myForm)
}

{ // inline object
const myForm = formAutoContent({
field1: 'value1',
field2: ['value2', 'value2.2']
}, { payload: 'body', headers: 'head' } as const);

expectAssignable<{ body: Readable, head: Record<string, string> }>(myForm)
}

{ // inline object with payload property
const myForm = formAutoContent({
field1: 'value1',
field2: ['value2', 'value2.2']
}, { payload: 'body' } as const);

expectAssignable<{ body: Readable, headers: Record<string, string> }>(myForm)
}

{ // inline object with headers property
const myForm = formAutoContent({
field1: 'value1',
field2: ['value2', 'value2.2']
}, { headers: 'head' } as const);

expectAssignable<{ payload: Readable, head: Record<string, string> }>(myForm)
}

{ // additional properties are not allowed
formAutoContent({
field1: 'value1',
field2: ['value2', 'value2.2']
// @ts-expect-error
}, { headers: 'head', foo: '' } as const);

formAutoContent({
field1: 'value1',
field2: ['value2', 'value2.2']
// @ts-expect-error
}, { payload: 'body', foo: '' } as const);

formAutoContent({
field1: 'value1',
field2: ['value2', 'value2.2']
// @ts-expect-error
}, { foo: '' } as const);
}

0 comments on commit a535a07

Please sign in to comment.