├── .editorconfig ├── .eslintrc ├── .gitignore ├── .nvmrc ├── LICENSE ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── __fixtures__ │ ├── builtins │ │ ├── array.ts │ │ ├── date.ts │ │ ├── map.ts │ │ ├── optional-date.ts │ │ ├── optional-map.ts │ │ ├── optional-set.ts │ │ └── set.ts │ ├── functions │ │ ├── basic.ts │ │ └── generic.ts │ ├── objects │ │ ├── basic.ts │ │ ├── strips-functions.ts │ │ └── strips-nested-functions.ts │ ├── primitive │ │ ├── boolean.ts │ │ ├── null.ts │ │ ├── number.ts │ │ ├── optional-boolean.ts │ │ ├── optional-null.ts │ │ ├── optional-number.ts │ │ ├── optional-string.ts │ │ └── string.ts │ └── realistic │ │ └── dated-value.ts ├── __snapshots__ │ ├── builtins.spec.ts.snap │ ├── functions.spec.ts.snap │ ├── objects.spec.ts.snap │ ├── primitive.spec.ts.snap │ └── realistic.spec.ts.snap ├── builtins.spec.ts ├── dto.ts ├── functions.spec.ts ├── jest-extensions.ts ├── objects.spec.ts ├── primitive.spec.ts ├── realistic.spec.ts └── test-helpers.ts ├── tsconfig.json └── tsconfig.typecheck.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.ts] 8 | tab_width = 2 9 | indent_style = space 10 | 11 | [*.json] 12 | tab_width = 2 13 | indent_style = space 14 | 15 | [*.md] 16 | insert_final_newline = false 17 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 2018, 6 | "sourceType": "module", 7 | "warnOnUnsupportedTypeScriptVersion": false 8 | }, 9 | "plugins": ["@typescript-eslint", "jest", "prettier"], 10 | "extends": [ 11 | "plugin:@typescript-eslint/recommended", 12 | "prettier/@typescript-eslint" 13 | ], 14 | "rules": { 15 | "no-unused-expressions": "off", 16 | "prefer-promise-reject-errors": "error", 17 | "@typescript-eslint/no-non-null-assertion": "error", 18 | "@typescript-eslint/explicit-function-return-type": [ 19 | "error", 20 | { 21 | "allowExpressions": true, 22 | "allowTypedFunctionExpressions": true, 23 | "allowHigherOrderFunctions": true, 24 | "allowConciseArrowFunctionExpressionsStartingWithVoid": true 25 | } 26 | ], 27 | "prettier/prettier": [ 28 | "error", 29 | { 30 | "tabWidth": 2, 31 | "useTabs": false, 32 | "semi": false, 33 | "singleQuote": true, 34 | "trailingComma": "all", 35 | "printWidth": 110 36 | } 37 | ], 38 | "@typescript-eslint/no-unused-vars": [ 39 | "error", 40 | { "ignoreRestSiblings": true } 41 | ], 42 | "@typescript-eslint/no-unused-expressions": [ 43 | "error", 44 | { 45 | "allowShortCircuit": true, 46 | "allowTernary": true 47 | } 48 | ] 49 | }, 50 | "overrides": [ 51 | { 52 | "files": ["**/test.ts", "test-helpers.ts", "jest-extensions.ts"], 53 | "rules": { 54 | "@typescript-eslint/no-non-null-assertion": "off", 55 | "@typescript-eslint/explicit-function-return-type": "off", 56 | "@typescript-eslint/explicit-module-boundary-types": "off" 57 | } 58 | } 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | lib 4 | .vscode 5 | coverage 6 | .npmrc 7 | json-schema 8 | .DS_Store 9 | icon.png 10 | .eslintcache 11 | codecov.sh 12 | .nyc_output 13 | 14 | black-box-tests/out 15 | black-box-tests/test-environment/**/* 16 | test-out 17 | *.tsbuildinfo 18 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 14 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Tamara Jordan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dto 2 | 3 | A generic typescript utility type that transforms a given type into something that can go over the wire. For example, Sets are turned into arrays and optional values (denoted by `?` or `| undefined`) are converted to null. 4 | 5 | I made this playground as a way to safely iterate on the DTO type without regression 6 | 7 | ## But why? 8 | 9 | Here's a blog post where I explain: https://dev.to/tamj0rd2/dto-a-typescript-utility-type-4o3m 10 | 11 | ## How this repo works 12 | 13 | The DTO is located in [src/dto.ts](./src/dto.ts) 14 | 15 | The tests are located in the __fixtures__ folder. 16 | 17 | Each test declares a type that uses a DTO. For example, `type numberDTO = DTO`. The lines following that are test cases, some of which I expect to cause errors and some of which I expect to compile successfully. 18 | 19 | For example, taking a look at /__fixtures__/primitive/optional-string: 20 | 21 | ```typescript 22 | import { Dto } from '~dto' 23 | 24 | // this is a DTO of a string 25 | export type StringDto = Dto 26 | 27 | // I expect this not to give compiler errors because a string can be sent over the wire without any transformations 28 | export const dtoGood: StringDto = 'Hello world!' 29 | 30 | // I expect this to fail because numbers are not strings 31 | export const dtoBadNumber: StringDto = 28947 32 | 33 | // I expect this to fail because we haven't made our string optional 34 | export const dtoBadUndefined: StringDto = undefined 35 | 36 | // I expect this to fail because bools are not strings 37 | export const dtoBadBool: StringDto = false 38 | 39 | // I expect this to fail because we haven't made our string optional 40 | export const dtoBadNull: StringDto = null 41 | 42 | // I expect this to fail because objects are not strings 43 | export const dtoBadObject: StringDto = { hello: 'world' } 44 | ``` 45 | 46 | If you open up the fixtures files in your editor, you should easily be able to see which lines pass and which lines fail 47 | 48 | Run `npm run test` to see and capture the typescript compiler output as a jest snapshot 49 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const commonSettings = { 2 | testPathIgnorePatterns: [ 3 | '/bin', 4 | '/coverage', 5 | '/lib/', 6 | '/node_modules/', 7 | ], 8 | collectCoverageFrom: ['/src/**/*.ts'], 9 | testEnvironment: 'node', 10 | watchPathIgnorePatterns: ['/test-out', '.*.js'], 11 | setupFilesAfterEnv: ['/src/jest-extensions.ts'], 12 | preset: 'ts-jest', 13 | } 14 | 15 | const lowLevelTestSettings = { 16 | moduleNameMapper: { 17 | '~(.*)$': '/src/$1', 18 | }, 19 | } 20 | 21 | const unitTestSettings = { 22 | ...commonSettings, 23 | ...lowLevelTestSettings, 24 | displayName: 'Unit', 25 | unmockedModulePathPatterns: [], 26 | coverageDirectory: './coverage/unit-tests', 27 | coverageReporters: ['lcov', 'text', 'text-summary'], 28 | } 29 | 30 | module.exports = unitTestSettings 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dto", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "pretest": "rm -rf tsconfig.tsbuildinfo", 8 | "test": "jest --runInBand", 9 | "typecheck": "tsc -b tsconfig.typecheck.json", 10 | "lint:base": "eslint --color --ignore-path .gitignore --cache", 11 | "lint": "npm run lint:base -- '**/*.ts' --fix" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "husky": { 17 | "hooks": { 18 | "pre-commit": "lint-staged" 19 | } 20 | }, 21 | "lint-staged": { 22 | "*.ts": [ 23 | "npm run lint:base -- --fix" 24 | ] 25 | }, 26 | "dependencies": { 27 | "@typescript-eslint/eslint-plugin": "^4.4.0", 28 | "@typescript-eslint/parser": "^4.4.0", 29 | "eslint": "^7.11.0", 30 | "eslint-config-prettier": "^6.12.0", 31 | "eslint-plugin-jest": "^24.1.0", 32 | "eslint-plugin-prettier": "^3.1.4", 33 | "prettier": "^2.1.2" 34 | }, 35 | "devDependencies": { 36 | "@types/jest": "^26.0.14", 37 | "@types/node": "^14.11.8", 38 | "husky": "^4.3.0", 39 | "jest": "^26.5.2", 40 | "lint-staged": "^10.4.0", 41 | "ts-jest": "^26.4.1", 42 | "typescript": "^4.0.3" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/__fixtures__/builtins/array.ts: -------------------------------------------------------------------------------- 1 | import { Dto } from '~dto' 2 | 3 | export type ArrayDto = Dto 4 | 5 | export const dtoGood: ArrayDto = [1, 2, 3] 6 | 7 | export const dtoBadSet: ArrayDto = new Set([1, 2, 3]) 8 | 9 | export const dtoBadNull: ArrayDto = null 10 | 11 | export const dtoBadUndefined: ArrayDto = undefined 12 | 13 | export const dtoBadString: ArrayDto = 'this is not good' 14 | 15 | export const dtoBadNumber: ArrayDto = 192843 16 | 17 | export const dtoBadObject: ArrayDto = { bad: 'times' } 18 | -------------------------------------------------------------------------------- /src/__fixtures__/builtins/date.ts: -------------------------------------------------------------------------------- 1 | import { Dto } from '~dto' 2 | 3 | export type DateDto = Dto 4 | 5 | export const dtoGood: DateDto = 'an iso date string' 6 | 7 | export const dtoBadDate: DateDto = new Date() 8 | 9 | export const dtoBadNull: DateDto = null 10 | 11 | export const dtoBadUndefined: DateDto = undefined 12 | 13 | export const dtoBadObject: DateDto = { bad: 'times' } 14 | -------------------------------------------------------------------------------- /src/__fixtures__/builtins/map.ts: -------------------------------------------------------------------------------- 1 | import { Dto } from '~dto' 2 | 3 | export type MapDto = Dto> 4 | 5 | export const dtoGood: MapDto = { item1: 1, item2: 2, item3: 3 } 6 | 7 | export const dtoBadMap: MapDto = new Map() 8 | 9 | export const dtoBadNull: MapDto = null 10 | 11 | export const dtoBadUndefined: MapDto = undefined 12 | 13 | export const dtoBadString: MapDto = 'this is not good' 14 | 15 | export const dtoBadNumber: MapDto = 192843 16 | 17 | export const dtoBadObject: MapDto = { bad: 'times' } 18 | -------------------------------------------------------------------------------- /src/__fixtures__/builtins/optional-date.ts: -------------------------------------------------------------------------------- 1 | import { Dto } from '~dto' 2 | 3 | export type OptionalDateDto = Dto 4 | 5 | export const dtoGood: OptionalDateDto = 'an iso date string' 6 | 7 | export const dtoGoodNull: OptionalDateDto = null 8 | 9 | export const dtoBadDate: OptionalDateDto = new Date() 10 | 11 | export const dtoBadUndefined: OptionalDateDto = undefined 12 | 13 | export const dtoBadObject: OptionalDateDto = { bad: 'times' } 14 | -------------------------------------------------------------------------------- /src/__fixtures__/builtins/optional-map.ts: -------------------------------------------------------------------------------- 1 | import { Dto } from '~dto' 2 | 3 | export type MapDto = Dto | undefined> 4 | 5 | export const dtoGood: MapDto = { item1: 1, item2: 2, item3: 3 } 6 | 7 | export const dtoGoodNull: MapDto = null 8 | 9 | export const dtoBadMap: MapDto = new Map() 10 | 11 | export const dtoBadUndefined: MapDto = undefined 12 | 13 | export const dtoBadString: MapDto = 'this is not good' 14 | 15 | export const dtoBadNumber: MapDto = 192843 16 | 17 | export const dtoBadObject: MapDto = { bad: 'times' } 18 | -------------------------------------------------------------------------------- /src/__fixtures__/builtins/optional-set.ts: -------------------------------------------------------------------------------- 1 | import { Dto } from '~dto' 2 | 3 | export type OptionalSetDto = Dto | undefined> 4 | 5 | export const dtoGood: OptionalSetDto = [1, 2, 3] 6 | 7 | export const dtoGoodNull: OptionalSetDto = null 8 | 9 | export const dtoBadSet: OptionalSetDto = new Set([1, 2, 3]) 10 | 11 | export const dtoBadUndefined: OptionalSetDto = undefined 12 | 13 | export const dtoBadString: OptionalSetDto = 'this is not good' 14 | 15 | export const dtoBadNumber: OptionalSetDto = 192843 16 | 17 | export const dtoBadObject: OptionalSetDto = { bad: 'times' } 18 | -------------------------------------------------------------------------------- /src/__fixtures__/builtins/set.ts: -------------------------------------------------------------------------------- 1 | import { Dto } from '~dto' 2 | 3 | export type SetDto = Dto> 4 | 5 | export const dtoGood: SetDto = [1, 2, 3] 6 | 7 | export const dtoBadSet: SetDto = new Set([1, 2, 3]) 8 | 9 | export const dtoBadNull: SetDto = null 10 | 11 | export const dtoBadUndefined: SetDto = undefined 12 | 13 | export const dtoBadString: SetDto = 'this is not good' 14 | 15 | export const dtoBadNumber: SetDto = 192843 16 | 17 | export const dtoBadObject: SetDto = { bad: 'times' } 18 | -------------------------------------------------------------------------------- /src/__fixtures__/functions/basic.ts: -------------------------------------------------------------------------------- 1 | import { Dto } from '~dto' 2 | 3 | type MyFunc = (name: string, age: number) => [string, number] 4 | 5 | export type FunctionDto = Dto 6 | 7 | // this should error because functions cannot be serialized 8 | export const dtoBad: FunctionDto = (name: string, age: number): [string, number] => [name, age] 9 | 10 | // the rest of these should all error because we never want functions to go over 11 | // the wire at all. FunctionDto should be of type never 12 | export const dtoBadNull: FunctionDto = null 13 | 14 | export const dtoBadUndefined: FunctionDto = undefined 15 | 16 | export const dtoBadString: FunctionDto = '123897' 17 | 18 | export const dtoBadNumber: FunctionDto = 128376 19 | 20 | export const dtoBadObject: FunctionDto = { bad: 'times' } 21 | -------------------------------------------------------------------------------- /src/__fixtures__/functions/generic.ts: -------------------------------------------------------------------------------- 1 | import { Dto } from '~dto' 2 | 3 | type MyFunc = (name: string, age: number) => R 4 | 5 | export type GenericFunctionDto = Dto> 6 | 7 | // this should error because functions cannot be serialized 8 | export const dtoBad: GenericFunctionDto<[string, number]> = (name: string, age: number): [string, number] => [ 9 | name, 10 | age, 11 | ] 12 | 13 | // the rest of these should all error because we never want functions to go over 14 | // the wire at all. FunctionDto should be of type never 15 | export const dtoBadNull: GenericFunctionDto<[string, number]> = null 16 | 17 | export const dtoBadUndefined: GenericFunctionDto<[string, number]> = undefined 18 | 19 | export const dtoBadString: GenericFunctionDto<[string, number]> = '123897' 20 | 21 | export const dtoBadNumber: GenericFunctionDto<[string, number]> = 128376 22 | 23 | export const dtoBadObject: GenericFunctionDto<[string, number]> = { bad: 'times' } 24 | -------------------------------------------------------------------------------- /src/__fixtures__/objects/basic.ts: -------------------------------------------------------------------------------- 1 | import { Dto } from '~dto' 2 | 3 | type MyObj = { name: string; age: number; favouriteFilms: string[] } 4 | type ObjectDto = Dto 5 | 6 | export const dtoGood: ObjectDto = { age: 1, name: 'jeff', favouriteFilms: ['film1', 'film2'] } 7 | 8 | export const dtoBadMissingProperties: ObjectDto = { age: 1, favouriteFilms: [] } 9 | 10 | export const dtoBadExtraProperties: ObjectDto = { age: 1, name: 'jeff', favouriteFilms: [], location: 'NY' } 11 | 12 | export const dtoBadNull: ObjectDto = null 13 | 14 | export const dtoBadUndefined: ObjectDto = undefined 15 | 16 | export const dtoBadString: ObjectDto = '123897' 17 | 18 | export const dtoBadNumber: ObjectDto = 128376 19 | 20 | export const dtoBadObject: ObjectDto = { bad: 'times' } 21 | 22 | interface MyDeepObj { 23 | hello1: string 24 | world1: Date 25 | nested1: { 26 | hello2: string 27 | world2: Date 28 | nested2: { 29 | hello3: string 30 | world3: Date 31 | } 32 | } 33 | } 34 | 35 | type DeepObjectDTO = Dto 36 | 37 | export const goodDeepObject: DeepObjectDTO = { 38 | hello1: 'hello1', 39 | world1: 'my iso date', 40 | nested1: { 41 | hello2: 'hello2', 42 | world2: 'my iso date', 43 | nested2: { 44 | hello3: 'hello3', 45 | world3: 'my iso date', 46 | }, 47 | }, 48 | } 49 | 50 | export const badDeepObject: DeepObjectDTO = { 51 | hello1: 'hello1', 52 | world1: new Date(), 53 | nested1: { 54 | hello2: 'hello2', 55 | world2: new Date(), 56 | nested2: { 57 | hello3: 'hello3', 58 | world3: new Date(), 59 | }, 60 | }, 61 | } 62 | -------------------------------------------------------------------------------- /src/__fixtures__/objects/strips-functions.ts: -------------------------------------------------------------------------------- 1 | import { Dto } from '~dto' 2 | 3 | type MyObj = { name: string; age: number; speak(): void } 4 | type ObjectDto = Dto 5 | 6 | export const dtoGood: ObjectDto = { age: 1, name: 'jeff' } 7 | 8 | export const dtoBadWithFunc: ObjectDto = { age: 1, name: 'jeff', speak: () => void undefined } 9 | 10 | export const dtoBadMissingProperties: ObjectDto = { age: 1 } 11 | 12 | export const dtoBadNull: ObjectDto = null 13 | 14 | export const dtoBadUndefined: ObjectDto = undefined 15 | -------------------------------------------------------------------------------- /src/__fixtures__/objects/strips-nested-functions.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 2 | import { Dto } from '~dto' 3 | 4 | interface MyObjWithNestedFuncs { 5 | hello1: string 6 | world1(): void 7 | nested1: { 8 | hello2: string 9 | world2(): void 10 | nested2: { 11 | hello3: string 12 | world3(): void 13 | } 14 | } 15 | } 16 | 17 | type ObjectDto = Dto 18 | 19 | export const dtoGood: ObjectDto = { 20 | hello1: 'hello1', 21 | nested1: { 22 | hello2: 'hello2', 23 | nested2: { 24 | hello3: 'hello3', 25 | }, 26 | }, 27 | } 28 | 29 | export const dtoBadWithFuncs: ObjectDto = { 30 | hello1: 'hello1', 31 | world1: () => void undefined, 32 | nested1: { 33 | hello2: 'hello2', 34 | world2: () => void undefined, 35 | nested2: { 36 | hello3: 'hello3', 37 | world3: () => void undefined, 38 | }, 39 | }, 40 | } 41 | 42 | export const dtoBadWithOnlyNestedFuncs: ObjectDto = { 43 | hello1: 'hello1', 44 | nested1: { 45 | hello2: 'hello2', 46 | world2: () => void undefined, 47 | nested2: { 48 | hello3: 'hello3', 49 | world3: () => void undefined, 50 | }, 51 | }, 52 | } 53 | 54 | export const dtoBadWithOnlyLayer1Func: ObjectDto = { 55 | hello1: 'hello1', 56 | world1: () => void undefined, 57 | nested1: { 58 | hello2: 'hello2', 59 | nested2: { 60 | hello3: 'hello3', 61 | }, 62 | }, 63 | } 64 | 65 | export const dtoBadWithOnlyLayer2Func: ObjectDto = { 66 | hello1: 'hello1', 67 | nested1: { 68 | hello2: 'hello2', 69 | world2: () => void undefined, 70 | nested2: { 71 | hello3: 'hello3', 72 | }, 73 | }, 74 | } 75 | 76 | export const dtoBadWithOnlyDeeplyNestedFuncs: ObjectDto = { 77 | hello1: 'hello1', 78 | nested1: { 79 | hello2: 'hello2', 80 | nested2: { 81 | hello3: 'hello3', 82 | world3: () => void undefined, 83 | }, 84 | }, 85 | } 86 | 87 | export const dtoBadNull: ObjectDto = null 88 | 89 | export const dtoBadUndefined: ObjectDto = undefined 90 | -------------------------------------------------------------------------------- /src/__fixtures__/primitive/boolean.ts: -------------------------------------------------------------------------------- 1 | import { Dto } from '~dto' 2 | 3 | export type BooleanDto = Dto 4 | 5 | export const dtoGood: BooleanDto = false 6 | 7 | export const dtoBadNull: BooleanDto = null 8 | 9 | export const dtoBadUndefined: BooleanDto = undefined 10 | 11 | export const dtoBadString: BooleanDto = '123897' 12 | 13 | export const dtoBadObject: BooleanDto = { bad: 'times' } 14 | -------------------------------------------------------------------------------- /src/__fixtures__/primitive/null.ts: -------------------------------------------------------------------------------- 1 | import { Dto } from '~dto' 2 | 3 | export type NullDto = Dto 4 | 5 | export const dtoGood: NullDto = null 6 | 7 | export const dtoBadUndefined: NullDto = undefined 8 | 9 | export const dtoBadNumber: NullDto = 123 10 | 11 | export const dtoBadObject: NullDto = { hey: 'lol' } 12 | -------------------------------------------------------------------------------- /src/__fixtures__/primitive/number.ts: -------------------------------------------------------------------------------- 1 | import { Dto } from '~dto' 2 | 3 | export type NumberDto = Dto 4 | 5 | export const dtoGood: NumberDto = 123123 6 | 7 | export const dtoBadNull: NumberDto = null 8 | 9 | export const dtoBadUndefined: NumberDto = undefined 10 | 11 | export const dtoBadString: NumberDto = '123897' 12 | 13 | export const dtoBadObject: NumberDto = { bad: 'times' } 14 | -------------------------------------------------------------------------------- /src/__fixtures__/primitive/optional-boolean.ts: -------------------------------------------------------------------------------- 1 | import { Dto } from '~dto' 2 | 3 | export type OptionalBooleanDto = Dto 4 | 5 | export const dtoGood: OptionalBooleanDto = false 6 | 7 | export const dtoGoodNull: OptionalBooleanDto = null 8 | 9 | export const dtoBadUndefined: OptionalBooleanDto = undefined 10 | 11 | export const dtoBadString: OptionalBooleanDto = '123897' 12 | 13 | export const dtoBadObject: OptionalBooleanDto = { bad: 'times' } 14 | -------------------------------------------------------------------------------- /src/__fixtures__/primitive/optional-null.ts: -------------------------------------------------------------------------------- 1 | import { Dto } from '~dto' 2 | 3 | export type OptionalNullDto = Dto 4 | 5 | export const dtoGood: OptionalNullDto = null 6 | 7 | export const dtoBadUndefined: OptionalNullDto = undefined 8 | 9 | export const dtoBadNumber: OptionalNullDto = 123 10 | 11 | export const dtoBadObject: OptionalNullDto = { hey: 'lol' } 12 | -------------------------------------------------------------------------------- /src/__fixtures__/primitive/optional-number.ts: -------------------------------------------------------------------------------- 1 | import { Dto } from '~dto' 2 | 3 | export type OptionalNumberDto = Dto 4 | 5 | export const dtoGood: OptionalNumberDto = 123123 6 | 7 | export const dtoGoodNull: OptionalNumberDto = null 8 | 9 | export const dtoBadUndefined: OptionalNumberDto = undefined 10 | 11 | export const dtoBadString: OptionalNumberDto = '123897' 12 | 13 | export const dtoBadObject: OptionalNumberDto = { bad: 'times' } 14 | -------------------------------------------------------------------------------- /src/__fixtures__/primitive/optional-string.ts: -------------------------------------------------------------------------------- 1 | import { Dto } from '~dto' 2 | 3 | export type OptionalStringDto = Dto 4 | 5 | export const dtoGood: OptionalStringDto = 'Hello world!' 6 | 7 | export const dtoGoodNull: OptionalStringDto = null 8 | 9 | export const dtoBadNumber: OptionalStringDto = 28947 10 | 11 | export const dtoBadUndefined: OptionalStringDto = undefined 12 | 13 | export const dtoBadBool: OptionalStringDto = false 14 | 15 | export const dtoBadObject: OptionalStringDto = { hello: 'world' } 16 | -------------------------------------------------------------------------------- /src/__fixtures__/primitive/string.ts: -------------------------------------------------------------------------------- 1 | import { Dto } from '~dto' 2 | 3 | export type StringDto = Dto 4 | 5 | export const dtoGood: StringDto = 'Hello world!' 6 | 7 | export const dtoBadNumber: StringDto = 28947 8 | 9 | export const dtoBadUndefined: StringDto = undefined 10 | 11 | export const dtoBadBool: StringDto = false 12 | 13 | export const dtoBadNull: StringDto = null 14 | 15 | export const dtoBadObject: StringDto = { hello: 'world' } 16 | -------------------------------------------------------------------------------- /src/__fixtures__/realistic/dated-value.ts: -------------------------------------------------------------------------------- 1 | import { Dto, Serializable } from '~dto' 2 | 3 | interface DatedValue { 4 | date: Date 5 | value: T 6 | sourceLink: string 7 | } 8 | 9 | type DatedValueDto = Dto> 10 | 11 | export const goodStringDatedValue: DatedValueDto = { 12 | date: 'iso date string', 13 | sourceLink: 'a sourcelink', 14 | value: 'a value', 15 | } 16 | 17 | export const badStringDatedValue: DatedValueDto = { 18 | date: 'iso date string', 19 | sourceLink: 'a sourcelink', 20 | value: 238947, 21 | } 22 | 23 | export const goodNumberDatedValue: DatedValueDto = { 24 | date: 'iso date string', 25 | sourceLink: 'a sourcelink', 26 | value: 123, 27 | } 28 | 29 | export const badNumberDatedValue: DatedValueDto = { 30 | date: 'iso date string', 31 | sourceLink: 'a sourcelink', 32 | value: 'not a number', 33 | } 34 | 35 | type MyObj = { name: string; age: number } 36 | export const goodObjectDatedValue: DatedValueDto = { 37 | date: 'iso date string', 38 | sourceLink: 'a sourcelink', 39 | value: { age: 123, name: 'jeff' }, 40 | } 41 | 42 | export const badObjectDatedValue: DatedValueDto = { 43 | date: 'iso date string', 44 | sourceLink: 'a sourcelink', 45 | value: { grade: 123, subject: 'math' }, 46 | } 47 | 48 | export const goodFunctionDatedValue: DatedValueDto<() => void> = { 49 | date: 'date', 50 | sourceLink: 'source link', 51 | } 52 | 53 | export const badFunctionDatedValue: DatedValueDto<() => void> = { 54 | date: 'date', 55 | sourceLink: 'source link', 56 | value: () => void undefined, 57 | } 58 | 59 | type NestedDatedValue = { nested: DatedValue } 60 | export const goodNestedDatedValue: Dto> = { 61 | nested: { date: 'iso date', sourceLink: 'sourceLink', value: 'value' }, 62 | } 63 | 64 | export const badNestedDatedValue: Dto> = { 65 | nested: { date: new Date(), sourceLink: 'sourceLink', value: 'value' }, 66 | } 67 | 68 | export const mapOfDatedValue: Dto>> = { 69 | goodItem: { date: 'my iso string', sourceLink: 'source', value: 'myvalue' }, 70 | badItem: { date: new Date(), sourceLink: 'source', value: 'myvalue' }, 71 | } 72 | 73 | // eslint-disable-next-line @typescript-eslint/ban-types 74 | export function serializeDatedValue(datedValue: DatedValue): never 75 | export function serializeDatedValue( 76 | datedValue: DatedValue, 77 | ): { date: string; value: Dto; sourceLink: string } 78 | export function serializeDatedValue( 79 | datedValue: DatedValue, 80 | ): { date: string; value: Dto; sourceLink: string } { 81 | if (typeof datedValue.value === 'function') throw new Error('Functions cannot be serialized') 82 | 83 | const isSerializable = (x: T): x is T & Serializable => 84 | typeof (x as Serializable).serialize === 'function' 85 | 86 | return { 87 | date: datedValue.date.toJSON(), 88 | sourceLink: datedValue.sourceLink, 89 | value: isSerializable(datedValue.value) ? datedValue.value.serialize() : (datedValue.value as Dto), 90 | } 91 | } 92 | 93 | export const goodSerializedFunctionDto: never = serializeDatedValue({ 94 | date: new Date(), 95 | sourceLink: 'sourceLink', 96 | value: () => void undefined, 97 | }) 98 | 99 | export const badSerializedFunctionDto: never = serializeDatedValue({ 100 | date: new Date(), 101 | sourceLink: 'sourceLink', 102 | }) 103 | 104 | export const goodSerializedStringDto: DatedValueDto = serializeDatedValue({ 105 | date: new Date(), 106 | sourceLink: 'sourceLInk', 107 | value: 'woah', 108 | }) 109 | 110 | export const badSerializedStringDto: DatedValueDto = serializeDatedValue({ 111 | date: new Date(), 112 | sourceLink: 'sourceLInk', 113 | value: 123123, 114 | }) 115 | 116 | type MyObjWithFn = { age: number; name: string; speak(): void } 117 | export const goodSerializedObjectDto: DatedValueDto = serializeDatedValue({ 118 | date: new Date(), 119 | sourceLink: 'sourceLink', 120 | value: { age: 123, name: 'asdf', speak: () => void undefined }, 121 | }) 122 | 123 | export const badSerializedObjectDto: DatedValueDto = serializeDatedValue({ 124 | date: new Date(), 125 | sourceLink: 'sourceLink', 126 | value: { score: 2384 }, 127 | }) 128 | -------------------------------------------------------------------------------- /src/__snapshots__/builtins.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`array.ts 1`] = ` 4 | Object { 5 | "errorCount": 6, 6 | "fileName": "builtins/array.ts", 7 | "messages": Array [ 8 | "builtins/array.ts:7(14) Type 'Set' is not assignable to type 'number[]'.", 9 | "builtins/array.ts:9(14) Type 'null' is not assignable to type 'number[]'.", 10 | "builtins/array.ts:11(14) Type 'undefined' is not assignable to type 'number[]'.", 11 | "builtins/array.ts:13(14) Type 'string' is not assignable to type 'number[]'.", 12 | "builtins/array.ts:15(14) Type 'number' is not assignable to type 'number[]'.", 13 | "builtins/array.ts:17(41) Type '{ bad: string; }' is not assignable to type 'number[]'. 14 | Object literal may only specify known properties, and 'bad' does not exist in type 'number[]'.", 15 | ], 16 | } 17 | `; 18 | 19 | exports[`date.ts 1`] = ` 20 | Object { 21 | "errorCount": 4, 22 | "fileName": "builtins/date.ts", 23 | "messages": Array [ 24 | "builtins/date.ts:7(14) Type 'Date' is not assignable to type 'string'.", 25 | "builtins/date.ts:9(14) Type 'null' is not assignable to type 'string'.", 26 | "builtins/date.ts:11(14) Type 'undefined' is not assignable to type 'string'.", 27 | "builtins/date.ts:13(14) Type '{ bad: string; }' is not assignable to type 'string'.", 28 | ], 29 | } 30 | `; 31 | 32 | exports[`map.ts 1`] = ` 33 | Object { 34 | "errorCount": 6, 35 | "fileName": "builtins/map.ts", 36 | "messages": Array [ 37 | "builtins/map.ts:7(14) Type 'Map' is not assignable to type 'Record'.", 38 | "builtins/map.ts:9(14) Type 'null' is not assignable to type 'Record'.", 39 | "builtins/map.ts:11(14) Type 'undefined' is not assignable to type 'Record'.", 40 | "builtins/map.ts:13(14) Type 'string' is not assignable to type 'Record'.", 41 | "builtins/map.ts:15(14) Type 'number' is not assignable to type 'Record'.", 42 | "builtins/map.ts:17(39) Type 'string' is not assignable to type 'number'.", 43 | ], 44 | } 45 | `; 46 | 47 | exports[`optional-date.ts 1`] = ` 48 | Object { 49 | "errorCount": 3, 50 | "fileName": "builtins/optional-date.ts", 51 | "messages": Array [ 52 | "builtins/optional-date.ts:9(14) Type 'Date' is not assignable to type 'string'.", 53 | "builtins/optional-date.ts:11(14) Type 'undefined' is not assignable to type 'string | null'.", 54 | "builtins/optional-date.ts:13(14) Type '{ bad: string; }' is not assignable to type 'string'.", 55 | ], 56 | } 57 | `; 58 | 59 | exports[`optional-map.ts 1`] = ` 60 | Object { 61 | "errorCount": 5, 62 | "fileName": "builtins/optional-map.ts", 63 | "messages": Array [ 64 | "builtins/optional-map.ts:9(14) Type 'Map' is not assignable to type 'Record'. 65 | Index signature is missing in type 'Map'.", 66 | "builtins/optional-map.ts:11(14) Type 'undefined' is not assignable to type 'Record | null'.", 67 | "builtins/optional-map.ts:13(14) Type '\\"this is not good\\"' is not assignable to type 'Record | null'.", 68 | "builtins/optional-map.ts:15(14) Type '192843' is not assignable to type 'Record | null'.", 69 | "builtins/optional-map.ts:17(39) Type 'string' is not assignable to type 'number'.", 70 | ], 71 | } 72 | `; 73 | 74 | exports[`optional-set.ts 1`] = ` 75 | Object { 76 | "errorCount": 5, 77 | "fileName": "builtins/optional-set.ts", 78 | "messages": Array [ 79 | "builtins/optional-set.ts:9(14) Type 'Set' is not assignable to type 'number[]'.", 80 | "builtins/optional-set.ts:11(14) Type 'undefined' is not assignable to type 'number[] | null'.", 81 | "builtins/optional-set.ts:13(14) Type '\\"this is not good\\"' is not assignable to type 'number[] | null'.", 82 | "builtins/optional-set.ts:15(14) Type '192843' is not assignable to type 'number[] | null'.", 83 | "builtins/optional-set.ts:17(47) Type '{ bad: string; }' is not assignable to type 'number[]'. 84 | Object literal may only specify known properties, and 'bad' does not exist in type 'number[]'.", 85 | ], 86 | } 87 | `; 88 | 89 | exports[`set.ts 1`] = ` 90 | Object { 91 | "errorCount": 6, 92 | "fileName": "builtins/set.ts", 93 | "messages": Array [ 94 | "builtins/set.ts:7(14) Type 'Set' is missing the following properties from type 'number[]': length, pop, push, concat, and 23 more.", 95 | "builtins/set.ts:9(14) Type 'null' is not assignable to type 'number[]'.", 96 | "builtins/set.ts:11(14) Type 'undefined' is not assignable to type 'number[]'.", 97 | "builtins/set.ts:13(14) Type 'string' is not assignable to type 'number[]'.", 98 | "builtins/set.ts:15(14) Type 'number' is not assignable to type 'number[]'.", 99 | "builtins/set.ts:17(39) Type '{ bad: string; }' is not assignable to type 'number[]'. 100 | Object literal may only specify known properties, and 'bad' does not exist in type 'number[]'.", 101 | ], 102 | } 103 | `; 104 | -------------------------------------------------------------------------------- /src/__snapshots__/functions.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`basic.ts 1`] = ` 4 | Object { 5 | "errorCount": 6, 6 | "fileName": "functions/basic.ts", 7 | "messages": Array [ 8 | "functions/basic.ts:8(14) Type '(name: string, age: number) => [string, number]' is not assignable to type 'never'.", 9 | "functions/basic.ts:12(14) Type 'null' is not assignable to type 'never'.", 10 | "functions/basic.ts:14(14) Type 'undefined' is not assignable to type 'never'.", 11 | "functions/basic.ts:16(14) Type 'string' is not assignable to type 'never'.", 12 | "functions/basic.ts:18(14) Type 'number' is not assignable to type 'never'.", 13 | "functions/basic.ts:20(44) Type 'string' is not assignable to type 'never'.", 14 | ], 15 | } 16 | `; 17 | 18 | exports[`generic.ts 1`] = ` 19 | Object { 20 | "errorCount": 6, 21 | "fileName": "functions/generic.ts", 22 | "messages": Array [ 23 | "functions/generic.ts:8(14) Type '(name: string, age: number) => [string, number]' is not assignable to type 'never'.", 24 | "functions/generic.ts:15(14) Type 'null' is not assignable to type 'never'.", 25 | "functions/generic.ts:17(14) Type 'undefined' is not assignable to type 'never'.", 26 | "functions/generic.ts:19(14) Type 'string' is not assignable to type 'never'.", 27 | "functions/generic.ts:21(14) Type 'number' is not assignable to type 'never'.", 28 | "functions/generic.ts:23(69) Type 'string' is not assignable to type 'never'.", 29 | ], 30 | } 31 | `; 32 | -------------------------------------------------------------------------------- /src/__snapshots__/objects.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`basic.ts 1`] = ` 4 | Object { 5 | "errorCount": 10, 6 | "fileName": "objects/basic.ts", 7 | "messages": Array [ 8 | "objects/basic.ts:8(14) Property 'name' is missing in type '{ age: number; favouriteFilms: never[]; }' but required in type '{ name: string; age: number; favouriteFilms: string[]; }'.", 9 | "objects/basic.ts:10(93) Type '{ age: number; name: string; favouriteFilms: never[]; location: string; }' is not assignable to type '{ name: string; age: number; favouriteFilms: string[]; }'. 10 | Object literal may only specify known properties, and 'location' does not exist in type '{ name: string; age: number; favouriteFilms: string[]; }'.", 11 | "objects/basic.ts:12(14) Type 'null' is not assignable to type '{ name: string; age: number; favouriteFilms: string[]; }'.", 12 | "objects/basic.ts:14(14) Type 'undefined' is not assignable to type '{ name: string; age: number; favouriteFilms: string[]; }'.", 13 | "objects/basic.ts:16(14) Type 'string' is not assignable to type '{ name: string; age: number; favouriteFilms: string[]; }'.", 14 | "objects/basic.ts:18(14) Type 'number' is not assignable to type '{ name: string; age: number; favouriteFilms: string[]; }'.", 15 | "objects/basic.ts:20(42) Type '{ bad: string; }' is not assignable to type '{ name: string; age: number; favouriteFilms: string[]; }'. 16 | Object literal may only specify known properties, and 'bad' does not exist in type '{ name: string; age: number; favouriteFilms: string[]; }'.", 17 | "objects/basic.ts:52(3) Type 'Date' is not assignable to type 'string'.", 18 | "objects/basic.ts:55(5) Type 'Date' is not assignable to type 'string'.", 19 | "objects/basic.ts:58(7) Type 'Date' is not assignable to type 'string'.", 20 | ], 21 | } 22 | `; 23 | 24 | exports[`strips-functions.ts 1`] = ` 25 | Object { 26 | "errorCount": 4, 27 | "fileName": "objects/strips-functions.ts", 28 | "messages": Array [ 29 | "objects/strips-functions.ts:8(66) Type '{ age: number; name: string; speak: () => undefined; }' is not assignable to type '{ name: string; age: number; }'. 30 | Object literal may only specify known properties, and 'speak' does not exist in type '{ name: string; age: number; }'.", 31 | "objects/strips-functions.ts:10(14) Property 'name' is missing in type '{ age: number; }' but required in type '{ name: string; age: number; }'.", 32 | "objects/strips-functions.ts:12(14) Type 'null' is not assignable to type '{ name: string; age: number; }'.", 33 | "objects/strips-functions.ts:14(14) Type 'undefined' is not assignable to type '{ name: string; age: number; }'.", 34 | ], 35 | } 36 | `; 37 | 38 | exports[`strips-nested-functions.ts 1`] = ` 39 | Object { 40 | "errorCount": 7, 41 | "fileName": "objects/strips-nested-functions.ts", 42 | "messages": Array [ 43 | "objects/strips-nested-functions.ts:37(7) Type '{ hello3: string; world3: () => undefined; }' is not assignable to type '{ hello3: string; }'. 44 | Object literal may only specify known properties, and 'world3' does not exist in type '{ hello3: string; }'.", 45 | "objects/strips-nested-functions.ts:49(7) Type '{ hello3: string; world3: () => undefined; }' is not assignable to type '{ hello3: string; }'. 46 | Object literal may only specify known properties, and 'world3' does not exist in type '{ hello3: string; }'.", 47 | "objects/strips-nested-functions.ts:56(3) Type '{ hello1: string; world1: () => undefined; nested1: { hello2: string; nested2: { hello3: string; }; }; }' is not assignable to type '{ hello1: string; nested1: { hello2: string; nested2: { hello3: string; }; }; }'. 48 | Object literal may only specify known properties, and 'world1' does not exist in type '{ hello1: string; nested1: { hello2: string; nested2: { hello3: string; }; }; }'.", 49 | "objects/strips-nested-functions.ts:69(5) Type '{ hello2: string; world2: () => undefined; nested2: { hello3: string; }; }' is not assignable to type '{ hello2: string; nested2: { hello3: string; }; }'. 50 | Object literal may only specify known properties, and 'world2' does not exist in type '{ hello2: string; nested2: { hello3: string; }; }'.", 51 | "objects/strips-nested-functions.ts:82(7) Type '{ hello3: string; world3: () => undefined; }' is not assignable to type '{ hello3: string; }'. 52 | Object literal may only specify known properties, and 'world3' does not exist in type '{ hello3: string; }'.", 53 | "objects/strips-nested-functions.ts:87(14) Type 'null' is not assignable to type '{ hello1: string; nested1: { hello2: string; nested2: { hello3: string; }; }; }'.", 54 | "objects/strips-nested-functions.ts:89(14) Type 'undefined' is not assignable to type '{ hello1: string; nested1: { hello2: string; nested2: { hello3: string; }; }; }'.", 55 | ], 56 | } 57 | `; 58 | -------------------------------------------------------------------------------- /src/__snapshots__/primitive.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`boolean.ts 1`] = ` 4 | Object { 5 | "errorCount": 4, 6 | "fileName": "primitive/boolean.ts", 7 | "messages": Array [ 8 | "primitive/boolean.ts:7(14) Type 'null' is not assignable to type 'boolean'.", 9 | "primitive/boolean.ts:9(14) Type 'undefined' is not assignable to type 'boolean'.", 10 | "primitive/boolean.ts:11(14) Type 'string' is not assignable to type 'boolean'.", 11 | "primitive/boolean.ts:13(14) Type '{ bad: string; }' is not assignable to type 'boolean'.", 12 | ], 13 | } 14 | `; 15 | 16 | exports[`null.ts 1`] = ` 17 | Object { 18 | "errorCount": 3, 19 | "fileName": "primitive/null.ts", 20 | "messages": Array [ 21 | "primitive/null.ts:7(14) Type 'undefined' is not assignable to type 'null'.", 22 | "primitive/null.ts:9(14) Type '123' is not assignable to type 'null'.", 23 | "primitive/null.ts:11(14) Type '{ hey: string; }' is not assignable to type 'null'.", 24 | ], 25 | } 26 | `; 27 | 28 | exports[`number.ts 1`] = ` 29 | Object { 30 | "errorCount": 4, 31 | "fileName": "primitive/number.ts", 32 | "messages": Array [ 33 | "primitive/number.ts:7(14) Type 'null' is not assignable to type 'number'.", 34 | "primitive/number.ts:9(14) Type 'undefined' is not assignable to type 'number'.", 35 | "primitive/number.ts:11(14) Type 'string' is not assignable to type 'number'.", 36 | "primitive/number.ts:13(14) Type '{ bad: string; }' is not assignable to type 'number'.", 37 | ], 38 | } 39 | `; 40 | 41 | exports[`optional-boolean.ts 1`] = ` 42 | Object { 43 | "errorCount": 3, 44 | "fileName": "primitive/optional-boolean.ts", 45 | "messages": Array [ 46 | "primitive/optional-boolean.ts:9(14) Type 'undefined' is not assignable to type 'boolean | null'.", 47 | "primitive/optional-boolean.ts:11(14) Type '\\"123897\\"' is not assignable to type 'boolean | null'.", 48 | "primitive/optional-boolean.ts:13(14) Type '{ bad: string; }' is not assignable to type 'boolean | null'. 49 | Type '{ bad: string; }' is not assignable to type 'true'.", 50 | ], 51 | } 52 | `; 53 | 54 | exports[`optional-null.ts 1`] = ` 55 | Object { 56 | "errorCount": 3, 57 | "fileName": "primitive/optional-null.ts", 58 | "messages": Array [ 59 | "primitive/optional-null.ts:7(14) Type 'undefined' is not assignable to type 'null'.", 60 | "primitive/optional-null.ts:9(14) Type '123' is not assignable to type 'null'.", 61 | "primitive/optional-null.ts:11(14) Type '{ hey: string; }' is not assignable to type 'null'.", 62 | ], 63 | } 64 | `; 65 | 66 | exports[`optional-number.ts 1`] = ` 67 | Object { 68 | "errorCount": 3, 69 | "fileName": "primitive/optional-number.ts", 70 | "messages": Array [ 71 | "primitive/optional-number.ts:9(14) Type 'undefined' is not assignable to type 'number | null'.", 72 | "primitive/optional-number.ts:11(14) Type '\\"123897\\"' is not assignable to type 'number | null'.", 73 | "primitive/optional-number.ts:13(14) Type '{ bad: string; }' is not assignable to type 'number'.", 74 | ], 75 | } 76 | `; 77 | 78 | exports[`optional-string.ts 1`] = ` 79 | Object { 80 | "errorCount": 4, 81 | "fileName": "primitive/optional-string.ts", 82 | "messages": Array [ 83 | "primitive/optional-string.ts:9(14) Type '28947' is not assignable to type 'string | null'.", 84 | "primitive/optional-string.ts:11(14) Type 'undefined' is not assignable to type 'string | null'.", 85 | "primitive/optional-string.ts:13(14) Type 'false' is not assignable to type 'string | null'.", 86 | "primitive/optional-string.ts:15(14) Type '{ hello: string; }' is not assignable to type 'string'.", 87 | ], 88 | } 89 | `; 90 | 91 | exports[`string.ts 1`] = ` 92 | Object { 93 | "errorCount": 5, 94 | "fileName": "primitive/string.ts", 95 | "messages": Array [ 96 | "primitive/string.ts:7(14) Type 'number' is not assignable to type 'string'.", 97 | "primitive/string.ts:9(14) Type 'undefined' is not assignable to type 'string'.", 98 | "primitive/string.ts:11(14) Type 'boolean' is not assignable to type 'string'.", 99 | "primitive/string.ts:13(14) Type 'null' is not assignable to type 'string'.", 100 | "primitive/string.ts:15(14) Type '{ hello: string; }' is not assignable to type 'string'.", 101 | ], 102 | } 103 | `; 104 | -------------------------------------------------------------------------------- /src/__snapshots__/realistic.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`dated-value.ts 1`] = ` 4 | Object { 5 | "errorCount": 9, 6 | "fileName": "realistic/dated-value.ts", 7 | "messages": Array [ 8 | "realistic/dated-value.ts:20(3) Type 'number' is not assignable to type 'string'.", 9 | "realistic/dated-value.ts:32(3) Type 'string' is not assignable to type 'number'.", 10 | "realistic/dated-value.ts:45(12) Type '{ grade: number; subject: string; }' is not assignable to type '{ name: string; age: number; }'. 11 | Object literal may only specify known properties, and 'grade' does not exist in type '{ name: string; age: number; }'.", 12 | "realistic/dated-value.ts:56(3) Type '{ date: string; sourceLink: string; value: () => undefined; }' is not assignable to type '{ date: string; sourceLink: string; }'. 13 | Object literal may only specify known properties, and 'value' does not exist in type '{ date: string; sourceLink: string; }'.", 14 | "realistic/dated-value.ts:65(13) Type 'Date' is not assignable to type 'string'.", 15 | "realistic/dated-value.ts:70(14) Type 'Date' is not assignable to type 'string'.", 16 | "realistic/dated-value.ts:99(68) No overload matches this call. 17 | Overload 1 of 2, '(datedValue: DatedValue): never', gave the following error. 18 | Argument of type '{ date: Date; sourceLink: string; }' is not assignable to parameter of type 'DatedValue'. 19 | Property 'value' is missing in type '{ date: Date; sourceLink: string; }' but required in type 'DatedValue'. 20 | Overload 2 of 2, '(datedValue: DatedValue): { date: string; value: {}; sourceLink: string; }', gave the following error. 21 | Argument of type '{ date: Date; sourceLink: string; }' is not assignable to parameter of type 'DatedValue'. 22 | Property 'value' is missing in type '{ date: Date; sourceLink: string; }' but required in type 'DatedValue'.", 23 | "realistic/dated-value.ts:110(14) Type '{ date: string; value: number; sourceLink: string; }' is not assignable to type '{ date: string; value: string; sourceLink: string; }'. 24 | Types of property 'value' are incompatible. 25 | Type 'number' is not assignable to type 'string'.", 26 | "realistic/dated-value.ts:123(14) Type '{ date: string; value: { score: number; }; sourceLink: string; }' is not assignable to type '{ date: string; value: { name: string; age: number; }; sourceLink: string; }'. 27 | Types of property 'value' are incompatible. 28 | Type '{ score: number; }' is missing the following properties from type '{ name: string; age: number; }': name, age", 29 | ], 30 | } 31 | `; 32 | -------------------------------------------------------------------------------- /src/builtins.spec.ts: -------------------------------------------------------------------------------- 1 | import { runTests } from './test-helpers' 2 | 3 | runTests(__dirname, __filename) 4 | -------------------------------------------------------------------------------- /src/dto.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | 4 | type IsOptional = Extract extends never ? false : true 5 | export type Func = (...args: any[]) => any 6 | type IsFunction = T extends Func ? true : false 7 | type IsValueType = T extends 8 | | string 9 | | number 10 | | boolean 11 | | null 12 | | undefined 13 | | Func 14 | | Set 15 | | Map 16 | | Date 17 | | Array 18 | ? true 19 | : false 20 | 21 | type ReplaceDate = T extends Date ? string : T 22 | type ReplaceSet = T extends Set ? X[] : T 23 | type ReplaceMap = T extends Map 24 | ? Record< 25 | K extends string | number | symbol ? K : string, 26 | IsValueType extends true ? I : { [K in keyof ExcludeFuncsFromObj]: Dto } 27 | > 28 | : T 29 | type ReplaceArray = T extends Array ? Dto[] : T 30 | 31 | type ExcludeFuncsFromObj = Pick extends true ? never : K }[keyof T]> 32 | 33 | type Dtoified = IsValueType extends true 34 | ? ReplaceDate>>> 35 | : { [K in keyof ExcludeFuncsFromObj]: Dto } 36 | 37 | export type Dto = IsFunction extends true 38 | ? never 39 | : IsOptional extends true 40 | ? Dtoified> | null 41 | : Dtoified 42 | 43 | export type Serializable = T & { serialize(): Dto } 44 | -------------------------------------------------------------------------------- /src/functions.spec.ts: -------------------------------------------------------------------------------- 1 | import { runTests } from './test-helpers' 2 | 3 | runTests(__dirname, __filename) 4 | -------------------------------------------------------------------------------- /src/jest-extensions.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs' 2 | import { toMatchSnapshot } from 'jest-snapshot' 3 | import { resolve } from 'path' 4 | import ts from 'typescript' 5 | 6 | declare global { 7 | // eslint-disable-next-line @typescript-eslint/no-namespace 8 | namespace jest { 9 | interface Matchers { 10 | toMatchDiagnosticsSnapshot(): R 11 | } 12 | } 13 | } 14 | 15 | function getDiagnostics() { 16 | const diagnostics: ts.Diagnostic[] = [] 17 | const reportDiagnostic = (diagnostic: ts.Diagnostic) => diagnostics.push(diagnostic) 18 | const solutionBuilderHost = ts.createSolutionBuilderHost(ts.sys, undefined, reportDiagnostic) 19 | const solutionBuilder = ts.createSolutionBuilder(solutionBuilderHost, ['./tsconfig.json'], { 20 | incremental: true, 21 | }) 22 | solutionBuilder.build() 23 | 24 | return diagnostics.map((d) => { 25 | const message = ts.flattenDiagnosticMessageText(d.messageText, '\n') 26 | if (!d.file || !d.start) return { message } 27 | 28 | const { line, character } = d.file.getLineAndCharacterOfPosition(d.start) 29 | return { message, fileName: d.file.fileName, line: line + 1, character: character + 1 } 30 | }) 31 | } 32 | 33 | expect.extend({ 34 | toMatchDiagnosticsSnapshot(fileName: unknown) { 35 | if (typeof fileName !== 'string') { 36 | return { 37 | pass: false, 38 | message: () => 'Expected received value to be of type TestCase', 39 | } 40 | } 41 | 42 | const fileUnderTest = resolve('./src/__fixtures__', fileName) 43 | if (!existsSync(fileUnderTest)) { 44 | return { message: () => `File ${fileUnderTest} does not exist`, pass: false } 45 | } 46 | 47 | const diagnostics = getDiagnostics() 48 | const otherDiagnostics = diagnostics.filter((x) => !x.fileName) 49 | const fileDignostics = diagnostics 50 | .filter((d) => d.fileName === fileUnderTest) 51 | .map((d) => ({ ...d, fileName })) 52 | 53 | const messages = [ 54 | ...otherDiagnostics.map((d) => d.message), 55 | ...fileDignostics.map((d) => `${d.fileName}:${d.line}(${d.character}) ${d.message}`), 56 | ] 57 | const errorCount = messages.length 58 | 59 | const result = { 60 | fileName, 61 | errorCount, 62 | messages, 63 | } 64 | 65 | // @ts-expect-error dunno how to fix this "this" type error 66 | return toMatchSnapshot.call(this, result) 67 | }, 68 | }) 69 | -------------------------------------------------------------------------------- /src/objects.spec.ts: -------------------------------------------------------------------------------- 1 | import { runTests } from './test-helpers' 2 | 3 | runTests(__dirname, __filename) 4 | -------------------------------------------------------------------------------- /src/primitive.spec.ts: -------------------------------------------------------------------------------- 1 | import { runTests } from './test-helpers' 2 | 3 | runTests(__dirname, __filename) 4 | -------------------------------------------------------------------------------- /src/realistic.spec.ts: -------------------------------------------------------------------------------- 1 | import { runTests } from './test-helpers' 2 | 3 | runTests(__dirname, __filename) 4 | -------------------------------------------------------------------------------- /src/test-helpers.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { readdirSync } from 'fs' 3 | 4 | export const runTests = (testFolder: string, testPath: string) => { 5 | const fixturePrefix = testPath.replace(`${testFolder}/`, '').replace('.spec.ts', '') 6 | const files = readdirSync(resolve(process.cwd(), 'src', '__fixtures__', fixturePrefix)) 7 | 8 | files.forEach((fileName) => { 9 | test(fileName, () => { 10 | expect(`${fixturePrefix}/${fileName}`).toMatchDiagnosticsSnapshot() 11 | }) 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["esnext"], 5 | "moduleResolution": "node", 6 | "resolveJsonModule": true, 7 | "rootDir": "./src", 8 | "outDir": "./out", 9 | "baseUrl": "./", 10 | "typeRoots": ["./node_modules/@types"], 11 | "strict": true, 12 | "strictNullChecks": true, 13 | "noImplicitAny": true, 14 | "esModuleInterop": true, 15 | "allowSyntheticDefaultImports": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "noErrorTruncation": true, 18 | "noEmit": true, 19 | "paths": { 20 | "~dto": ["./src/dto.ts"] 21 | } 22 | }, 23 | "include": ["src/**/*.ts"], 24 | "exclude": ["node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.typecheck.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "src/__fixtures__"] 4 | } 5 | --------------------------------------------------------------------------------