├── .circleci └── config.yml ├── .commitlintrc.js ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmignore ├── .prettierignore ├── .prettierrc.js ├── CHANGELOG.md ├── README.md ├── babel.config.json ├── jest.config.json ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── camelcase.ts ├── index.ts ├── snakecase.ts └── types.ts ├── tests ├── camelcase.spec.ts └── snakecase.spec.ts └── tsconfig.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | defaults: &defaults 4 | working_directory: ~/repo 5 | docker: 6 | - image: circleci/node:14.8 7 | 8 | jobs: 9 | test: 10 | <<: *defaults 11 | steps: 12 | - checkout 13 | - run: npm install 14 | - run: npm test 15 | - save_cache: 16 | paths: 17 | - node_modules 18 | key: v1-dependencies-{{ checksum "package.json" }} 19 | - persist_to_workspace: 20 | root: ~/repo 21 | paths: . 22 | build: 23 | <<: *defaults 24 | steps: 25 | - add_ssh_keys: 26 | fingerprints: 'b9:ef:45:6e:fc:1b:03:f8:3c:56:48:d0:d7:59:fe:ea' 27 | - attach_workspace: 28 | at: ~/repo 29 | - restore_cache: 30 | keys: 31 | - v1-dependencies-{{ checksum "package.json" }} 32 | - v1-dependencies- 33 | - run: ls -al 34 | - run: 35 | name: Avoid hosts unknown for github 36 | command: mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config 37 | - run: npm run build 38 | - run: ls -al 39 | - save_cache: 40 | paths: 41 | - node_modules 42 | key: v1-dependencies-{{ checksum "package.json" }} 43 | - save_cache: 44 | paths: 45 | - dist 46 | key: dist 47 | # deploy: 48 | # <<: *defaults 49 | # steps: 50 | # - attach_workspace: 51 | # at: ~/repo 52 | # - restore_cache: 53 | # keys: 54 | # - dist 55 | # - v1-dependencies-{{ checksum "package.json" }} 56 | # - v1-dependencies- 57 | # - run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc 58 | # - run: ls -al 59 | # - run: # git config 60 | # name: git-config 61 | # command: | 62 | # git config --global user.email "1518190293@qq.com" 63 | # git config --global user.name "vnues" 64 | # - run: npm run release 65 | # - run: 66 | # name: Add github.com to known hosts 67 | # command: | 68 | # mkdir -p ~/.ssh 69 | # ssh-keyscan github.com >> ~/.ssh/known_hosts 70 | # - run: # git push 71 | # name: push-github 72 | # command: | 73 | # git checkout develop 74 | # git rebase master 75 | # git push 76 | # git push --tags 77 | # - run: npm publish 78 | 79 | workflows: 80 | version: 2 81 | build_test_and_deploy: 82 | jobs: 83 | - test: 84 | filters: 85 | tags: 86 | only: /.*/ 87 | - build: 88 | requires: 89 | - test 90 | filters: 91 | tags: 92 | only: /.*/ 93 | # - deploy: 94 | # requires: 95 | # - build 96 | # filters: 97 | # tags: 98 | # only: 99 | # - /^v.*/ 100 | # branches: 101 | # only: 102 | # master -------------------------------------------------------------------------------- /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | }; 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false 16 | 17 | [*.yml] 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | example 4 | es 5 | lib 6 | typings -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['alloy', 'alloy/typescript'], 3 | parserOptions: { 4 | ecmaVersion: 2018, 5 | sourceType: 'module', 6 | ecmaFeatures: { 7 | jsx: true, 8 | }, 9 | }, 10 | plugins: ['jest'], 11 | env: { 12 | jest: true, 13 | }, 14 | rules: { 15 | '@typescript-eslint/indent': 'off', 16 | '@typescript-eslint/explicit-member-accessibility': 'off', 17 | '@typescript-eslint/no-var-requires': 'off', 18 | '@typescript-eslint/consistent-type-assertions': 'off', 19 | '@typescript-eslint/typedef': 'off', 20 | 'no-new-func': 'off', 21 | '@typescript-eslint/no-empty-interface': 'off', 22 | complexity: 'off', 23 | '@typescript-eslint/no-this-alias': 'off', 24 | '@typescript-eslint/no-require-imports': 'off', 25 | '@typescript-eslint/prefer-optional-chain': 'off', 26 | 'max-params': 'off', 27 | 'no-param-reassign': 'off', 28 | 'no-trailing-spaces': [ 29 | 'error', 30 | { 31 | skipBlankLines: true, 32 | }, 33 | ], 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | .DS_Store 5 | *.log 6 | .vscode 7 | .idea 8 | dist 9 | compiled 10 | .awcache 11 | .rpt2_cache 12 | docs 13 | .cache 14 | typings 15 | es 16 | lib -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | npx --no-install commitlint --edit $1 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | npx lint-staged -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | tests 3 | .idea/ 4 | .github 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.svg 2 | **/*.ejs 3 | **/*.html 4 | package.json -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | // .prettierrc.js 2 | module.exports = { 3 | // max 120 characters per line 4 | printWidth: 120, 5 | // use 2 spaces for indentation 6 | tabWidth: 2, 7 | // use spaces instead of indentations 8 | useTabs: false, 9 | // semicolon at the end of the line 10 | semi: true, 11 | // use single quotes 12 | singleQuote: true, 13 | // object's key is quoted only when necessary 14 | quoteProps: 'as-needed', 15 | // use double quotes instead of single quotes in jsx 16 | jsxSingleQuote: false, 17 | // no comma at the end 18 | trailingComma: 'all', 19 | // spaces are required at the beginning and end of the braces 20 | bracketSpacing: true, 21 | // end tag of jsx need to wrap 22 | bracketSameLine: false, 23 | // brackets are required for arrow function parameter, even when there is only one parameter 24 | arrowParens: 'always', 25 | // format the entire contents of the file 26 | rangeStart: 0, 27 | rangeEnd: Infinity, 28 | // no need to write the beginning @prettier of the file 29 | requirePragma: false, 30 | // No need to automatically insert @prettier at the beginning of the file 31 | insertPragma: false, 32 | // use default break criteria 33 | proseWrap: 'preserve', 34 | // decide whether to break the html according to the display style 35 | htmlWhitespaceSensitivity: 'css', 36 | // vue files script and style tags indentation 37 | vueIndentScriptAndStyle: false, 38 | // lf for newline 39 | endOfLine: 'lf', 40 | // formats quoted code embedded 41 | embeddedLanguageFormatting: 'auto', 42 | }; 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [0.2.1](https://github.com/vnues/keys-transform-generator/compare/v0.0.2...v0.2.1) (2021-11-17) 6 | 7 | ### [0.0.2](https://github.com/vnues/keys-transform-generator/compare/v0.0.1...v0.0.2) (2021-11-17) 8 | 9 | ### 0.0.1 (2021-11-17) 10 | 11 | 12 | ### Features 13 | 14 | * 🎸 finish keys transform ([186779f](https://github.com/vnues/keys-transform-generator/commit/186779f4102d5d476c2a2840bfbc7cca86edd404)) 15 | * 🎸 init ([6ba2caf](https://github.com/vnues/keys-transform-generator/commit/6ba2caf50491826e0c2a279df75554a8caf6567d)) 16 | * finish keys transfrom module ([590512d](https://github.com/vnues/keys-transform-generator/commit/590512d07df59da18b63a6dc6303c2246d69be45)) 17 | 18 | 19 | ### Bug Fixes 20 | 21 | * 🐛 update ([b0af3ea](https://github.com/vnues/keys-transform-generator/commit/b0af3eaa3dd4189eb3a31c12b352f7f980d60cf0)) 22 | * fix files ([0fc6590](https://github.com/vnues/keys-transform-generator/commit/0fc659048a4abd68ebcd77e3c24717259950d168)) 23 | * ignore lib ([f8a3580](https://github.com/vnues/keys-transform-generator/commit/f8a3580af8da3777e2431d033812c58910d612de)) 24 | * remove config ([d687006](https://github.com/vnues/keys-transform-generator/commit/d687006f4a55cc3c43af3e979c747450f7f1e658)) 25 | 26 | ### 0.0.1 (2021-11-17) 27 | 28 | 29 | ### Features 30 | 31 | * 🎸 finish keys transform ([186779f](https://github.com/vnues/keys-transform-generator/commit/186779f4102d5d476c2a2840bfbc7cca86edd404)) 32 | * 🎸 init ([6ba2caf](https://github.com/vnues/keys-transform-generator/commit/6ba2caf50491826e0c2a279df75554a8caf6567d)) 33 | * finish keys transfrom module ([590512d](https://github.com/vnues/keys-transform-generator/commit/590512d07df59da18b63a6dc6303c2246d69be45)) 34 | 35 | 36 | ### Bug Fixes 37 | 38 | * 🐛 update ([b0af3ea](https://github.com/vnues/keys-transform-generator/commit/b0af3eaa3dd4189eb3a31c12b352f7f980d60cf0)) 39 | * fix files ([0fc6590](https://github.com/vnues/keys-transform-generator/commit/0fc659048a4abd68ebcd77e3c24717259950d168)) 40 | * ignore lib ([f8a3580](https://github.com/vnues/keys-transform-generator/commit/f8a3580af8da3777e2431d033812c58910d612de)) 41 | * remove config ([d687006](https://github.com/vnues/keys-transform-generator/commit/d687006f4a55cc3c43af3e979c747450f7f1e658)) 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # keys-transform-generator 2 | 3 | > 转换对象的 key,例如将 key 转化为驼峰形式,将 key 转化为下划线形式,适用于接口文档字段规范不统一的场景 4 | 5 | ## Install 6 | 7 | ```shell 8 | npm install keys-transform-generator 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```ts 14 | import { camelcaseKeys, snakecaseKeys } from '@src/index'; 15 | // 下划线转驼峰场景 16 | const data = { 17 | foo_bar: 2, 18 | foo_gar: 3, 19 | }; 20 | const options = { exclude: ['foo_gar'] as const }; 21 | const camelCaseData = camelcaseKeys(data, options); 22 | 23 | // 驼峰转下划线 24 | const data = { 25 | fooBar: 2, 26 | fooGar: 3, 27 | }; 28 | const options = { exclude: ['foo_gar'] as const }; 29 | const snakecaseData = snakecaseKeys(data, options); 30 | ``` 31 | 32 | ## Typescript Utils 33 | 34 | ### `SnakeCasedDeep` 35 | 36 | > 将接口的驼峰 key 转化为下划线 37 | 38 | ```ts 39 | import { SnakeCasedDeep } from '@src/index'; 40 | // 用法 41 | type RequestData = SnakeCasedDeep; 42 | ``` 43 | 44 | ### `CamelCasedDeep` 45 | 46 | > 将接口的下划线 key 转化为驼峰 47 | 48 | ```ts 49 | import { CamelCasedDeep } from '@src/index'; 50 | 51 | // 用法 52 | type ResponseData = CamelCasedDeep; 53 | ``` 54 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/env", "@babel/typescript"], 3 | "plugins": [ 4 | "@babel/plugin-transform-runtime", 5 | "@babel/plugin-proposal-class-properties" 6 | ], 7 | "env": { 8 | "esm": { 9 | "presets": [ 10 | [ 11 | "@babel/env", 12 | { 13 | "modules": false 14 | } 15 | ] 16 | ], 17 | "plugins": [ 18 | [ 19 | "@babel/plugin-transform-runtime", 20 | { 21 | "useESModules": true 22 | } 23 | ] 24 | ] 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "collectCoverage": true, 4 | "cacheDirectory": ".cache", 5 | "roots": [""], 6 | "transform": { 7 | "^.+\\.(t|j)s?x?$": "babel-jest" 8 | }, 9 | "testEnvironment": "node", 10 | "globals": {}, 11 | "moduleNameMapper": { 12 | "^@src/(.+)$": "/src/$1" 13 | }, 14 | "moduleFileExtensions": ["ts", "js"], 15 | "transformIgnorePatterns":["/node_modules/(?!(map-obj|quick-lru))"], 16 | "testRegex": "(test|spec)\\.(ts|js)?$" 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keys-transform-generator", 3 | "version": "0.2.1", 4 | "description": "keys transform", 5 | "author": "vnues", 6 | "license": "MIT", 7 | "keywords": [ 8 | "camelcase", 9 | "snakecase", 10 | "keys transform" 11 | ], 12 | "files": [ 13 | "dist", 14 | "es", 15 | "lib", 16 | "typings" 17 | ], 18 | "module": "es/index.js", 19 | "main": "dist/index.js", 20 | "typings": "typings/index.d.ts", 21 | "scripts": { 22 | "prepare": "husky install", 23 | "dev": "rollup -c rollup.config.js -w", 24 | "build": "rimraf dist & rollup -c rollup.config.js", 25 | "eslint:fix": "eslint --color --fix ./src", 26 | "format": "prettier --write ./src", 27 | "test": "jest --config jest.config.json", 28 | "gc": "git-cz", 29 | "release": "standard-version" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git@github.com:vnues/keys-transform-generator.git" 34 | }, 35 | "bugs": { 36 | "url": "https://github.com/vnues/keys-transform-generator" 37 | }, 38 | "homepage": "https://github.com/vnues/keys-transform-generator", 39 | "devDependencies": { 40 | "@babel/core": "^7.15.0", 41 | "@babel/plugin-proposal-class-properties": "^7.14.5", 42 | "@babel/plugin-transform-runtime": "^7.15.0", 43 | "@babel/preset-env": "^7.15.0", 44 | "@babel/preset-typescript": "^7.15.0", 45 | "@commitlint/cli": "^13.1.0", 46 | "@commitlint/config-conventional": "^13.1.0", 47 | "@rollup/plugin-babel": "^5.3.0", 48 | "@rollup/plugin-commonjs": "^20.0.0", 49 | "@rollup/plugin-node-resolve": "^13.0.4", 50 | "@types/jest": "^27.0.1", 51 | "@typescript-eslint/eslint-plugin": "^4.29.2", 52 | "@typescript-eslint/parser": "^4.29.2", 53 | "commitizen": "^4.2.4", 54 | "cz-conventional-changelog": "^3.3.0", 55 | "eslint": "^7.32.0", 56 | "eslint-config-alloy": "^4.1.0", 57 | "eslint-config-prettier": "^8.3.0", 58 | "eslint-plugin-jest": "^24.4.0", 59 | "eslint-plugin-prettier": "^3.4.0", 60 | "husky": "^7.0.1", 61 | "jest": "^25.3.0", 62 | "lint-staged": "^11.1.2", 63 | "prettier": "^2.3.2", 64 | "rimraf": "^3.0.2", 65 | "rollup": "^2.56.2", 66 | "rollup-plugin-dts": "^4.0.0", 67 | "rollup-plugin-terser": "^7.0.2", 68 | "rollup-plugin-typescript-paths": "^1.3.0", 69 | "standard-version": "^9.3.1", 70 | "type-fest": "^2.5.3", 71 | "typescript": "^4.4.4" 72 | }, 73 | "dependencies": { 74 | "camelcase": "^6.2.0", 75 | "map-obj": "^5.0.0", 76 | "snake-case": "^3.0.4" 77 | }, 78 | "lint-staged": { 79 | "src/**/*.ts?(x)": [ 80 | "eslint --fix", 81 | "prettier --write" 82 | ] 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 4 | import { terser } from 'rollup-plugin-terser'; 5 | import { typescriptPaths } from 'rollup-plugin-typescript-paths'; 6 | import dts from 'rollup-plugin-dts'; 7 | 8 | const extensions = ['.ts', '.js']; 9 | 10 | export default [ 11 | { 12 | input: 'src/index.ts', 13 | output: { 14 | file: 'dist/index.js', 15 | format: 'umd', 16 | name: 'keysTransformGenerator', 17 | sourcemap: false, 18 | }, 19 | plugins: [ 20 | babel({ 21 | exclude: 'node_modules/**', 22 | extensions, 23 | babelHelpers: 'runtime', 24 | }), 25 | nodeResolve({ browser: true, extensions }), 26 | typescriptPaths({ 27 | preserveExtensions: true, 28 | }), 29 | commonjs(), 30 | terser(), 31 | ], 32 | }, 33 | { 34 | input: 'src/index.ts', 35 | output: { 36 | file: 'lib/index.js', 37 | format: 'cjs', 38 | sourcemap: false, 39 | }, 40 | plugins: [ 41 | babel({ 42 | exclude: 'node_modules/**', 43 | extensions, 44 | babelHelpers: 'runtime', 45 | }), 46 | nodeResolve({ browser: true, extensions }), 47 | typescriptPaths({ 48 | preserveExtensions: true, 49 | }), 50 | commonjs(), 51 | ], 52 | external: ['map-obj', 'camelcase', 'snake-case'], 53 | }, 54 | { 55 | input: 'src/index.ts', 56 | output: { 57 | file: 'es/index.js', 58 | format: 'es', 59 | sourcemap: false, 60 | }, 61 | plugins: [ 62 | babel({ 63 | exclude: 'node_modules/**', 64 | extensions, 65 | babelHelpers: 'runtime', 66 | }), 67 | nodeResolve({ browser: true, extensions }), 68 | typescriptPaths({ 69 | preserveExtensions: true, 70 | }), 71 | commonjs(), 72 | ], 73 | external: ['map-obj', 'camelcase', 'snake-case'], 74 | }, 75 | { 76 | input: 'src/index.ts', 77 | output: [{ file: 'typings/index.d.ts', format: 'es' }], 78 | plugins: [ 79 | typescriptPaths({ 80 | preserveExtensions: true, 81 | }), 82 | dts({ respectExternal: ['type-fest'] }), 83 | ], 84 | }, 85 | ]; 86 | -------------------------------------------------------------------------------- /src/camelcase.ts: -------------------------------------------------------------------------------- 1 | import camelCase from 'camelcase'; 2 | import mapObject from 'map-obj'; 3 | 4 | import { CamelCaseKeysWithOptions, CamelCaseOptions } from '@src/types'; 5 | 6 | export function camelcaseKeys< 7 | T extends Record | readonly any[], 8 | Options extends CamelCaseOptions = CamelCaseOptions, 9 | >(input: T, options?: Options): CamelCaseKeysWithOptions { 10 | options = { deep: true, exclude: [], ...options } as Options; 11 | if (Array.isArray(input as readonly any[])) { 12 | return input.map((item: T) => camelcaseKeys(item, options)); 13 | } 14 | return mapObject( 15 | input as Record, 16 | (key: string, val: any) => [matches(options!.exclude!, key) ? key : camelCase(key), val], 17 | options, 18 | ) as CamelCaseKeysWithOptions; 19 | } 20 | 21 | function matches(patterns: ReadonlyArray, value: string): boolean { 22 | return patterns.some((pattern: string | RegExp) => { 23 | return typeof pattern === 'string' ? pattern === value : pattern.test(value); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './camelcase'; 2 | export * from './snakecase'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /src/snakecase.ts: -------------------------------------------------------------------------------- 1 | import mapObject from 'map-obj'; 2 | import { snakeCase } from 'snake-case'; 3 | import { SnakeCaseOptions, SnakeCaseWithOptions } from '@src/types'; 4 | 5 | export function snakecaseKeys< 6 | T extends Record | readonly any[], 7 | Options extends SnakeCaseOptions = SnakeCaseOptions, 8 | >(input: T, options?: Options): SnakeCaseWithOptions { 9 | options = { deep: true, exclude: [], ...options } as Options; 10 | if (Array.isArray(input as readonly any[])) { 11 | return input.map((item: T) => snakecaseKeys(item, options)); 12 | } 13 | return mapObject( 14 | input as Record, 15 | (key: string, val: any) => [matches(options!.exclude!, key) ? key : snakeCase(key), val], 16 | options, 17 | ) as SnakeCaseWithOptions; 18 | } 19 | 20 | function matches(patterns: ReadonlyArray, value: string): boolean { 21 | return patterns.some((pattern: string | RegExp) => { 22 | return typeof pattern === 'string' ? pattern === value : pattern.test(value); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { CamelCase, CamelCasedPropertiesDeep, SnakeCase, SnakeCasedPropertiesDeep } from 'type-fest'; 2 | 3 | /** 4 | Return a default type if input type is nil. 5 | @template T - Input type. 6 | @template U - Default type. 7 | */ 8 | export type WithDefault = T extends undefined | null ? U : T; 9 | 10 | export type EmptyTuple = []; 11 | 12 | /** 13 | Append a segment to dot-notation path. 14 | */ 15 | export type AppendPath = S extends '' ? Last : `${S}.${Last}`; 16 | 17 | /** 18 | Check if an element is included in a tuple. 19 | @template List - List of values. 20 | @template Target - Target to search. 21 | */ 22 | type Includes = List extends undefined 23 | ? false 24 | : List extends Readonly 25 | ? false 26 | : List extends readonly [infer First, ...infer Rest] 27 | ? First extends Target 28 | ? true 29 | : Includes 30 | : boolean; 31 | 32 | /** 33 | Convert keys of an object to camelcase strings. 34 | */ 35 | export interface CamelCaseOptions { 36 | /** 37 | Recurse nested objects and objects in arrays. 38 | @default false 39 | */ 40 | readonly deep?: boolean; 41 | /** 42 | Exclude keys from being camel-cased. 43 | If this option can be statically determined, it's recommended to add `as const` to it. 44 | @default [] 45 | */ 46 | readonly exclude?: ReadonlyArray; 47 | } 48 | 49 | type CamelCaseKeys< 50 | T extends Record | readonly any[], 51 | Deep extends boolean, 52 | Exclude extends readonly unknown[], 53 | Path extends string = '', 54 | > = T extends readonly any[] 55 | ? // Handle arrays or tuples. 56 | { 57 | [P in keyof T]: CamelCaseKeys; 58 | } 59 | : T extends Record 60 | ? // Handle objects. 61 | { 62 | // P是在keyof T 并且这个P(T的key)是string类型 我们把他断言成布尔类型 63 | // T & string 这是排除 symbol 和 number 类型 64 | // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#key-remapping-in-mapped-types 65 | // as [Includes 在input是新语法 在Typescript4.1新增的 66 | // Until now, mapped types could only produce new object types with keys that you provided them; however, lots of the time you want to be able to create new keys, or filter out keys, based on the inputs. 67 | [P in keyof T & string as [Includes] extends [true] ? P : CamelCase

]: [Deep] extends [true] 68 | ? T[P] extends Record 69 | ? CamelCaseKeys> 70 | : T[P] 71 | : T[P]; 72 | } 73 | : // Return anything else as-is. 74 | T; 75 | 76 | export type CamelCaseKeysWithOptions< 77 | T extends Record | readonly any[], 78 | Options extends CamelCaseOptions = CamelCaseOptions, 79 | > = CamelCaseKeys< 80 | T, 81 | // 根据不同的参数返回不同结果的TS类型 82 | WithDefault, 83 | WithDefault 84 | >; 85 | 86 | type SnakeCaseKeys< 87 | T extends Record | readonly any[], 88 | Deep extends boolean, 89 | Exclude extends readonly unknown[], 90 | Path extends string = '', 91 | > = T extends readonly any[] 92 | ? // Handle arrays or tuples. 93 | { 94 | [P in keyof T]: SnakeCaseKeys; 95 | } 96 | : T extends Record 97 | ? // Handle objects. 98 | { 99 | // P是在keyof T 并且这个P(T的key)是string类型 我们把他断言成布尔类型 100 | // T & string 这是排除 symbol 和 number 类型 101 | // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#key-remapping-in-mapped-types 102 | // as [Includes 在input是新语法 在Typescript4.1新增的 103 | // Until now, mapped types could only produce new object types with keys that you provided them; however, lots of the time you want to be able to create new keys, or filter out keys, based on the inputs. 104 | [P in keyof T & string as [Includes] extends [true] ? P : SnakeCase

]: [Deep] extends [true] 105 | ? T[P] extends Record 106 | ? SnakeCaseKeys> 107 | : T[P] 108 | : T[P]; 109 | } 110 | : // Return anything else as-is. 111 | T; 112 | 113 | export interface SnakeCaseOptions { 114 | /** 115 | Recurse nested objects and objects in arrays. 116 | @default true 117 | */ 118 | readonly deep?: boolean; 119 | /** 120 | Exclude keys from being snakeCased. 121 | @default [] 122 | */ 123 | readonly exclude?: ReadonlyArray; 124 | } 125 | 126 | export type SnakeCaseWithOptions< 127 | T extends Record | readonly any[], 128 | Options extends SnakeCaseOptions, 129 | > = SnakeCaseKeys, WithDefault>; 130 | 131 | export type SnakeCasedDeep = SnakeCasedPropertiesDeep; 132 | 133 | export type CamelCasedDeep = CamelCasedPropertiesDeep; 134 | -------------------------------------------------------------------------------- /tests/camelcase.spec.ts: -------------------------------------------------------------------------------- 1 | import { camelcaseKeys } from '@src/index'; 2 | 3 | describe('camelcase', () => { 4 | it('main', () => { 5 | const data = { 6 | 'foo-bar': 2, 7 | }; 8 | const camelCaseData = camelcaseKeys(data); 9 | expect(camelCaseData.fooBar).toBe(2); 10 | }); 11 | it('exclude option', () => { 12 | const data = { 13 | foo_bar: 2, 14 | foo_gar: 3, 15 | }; 16 | const options = { exclude: ['foo_gar'] as const }; 17 | const camelCaseData = camelcaseKeys(data, options); 18 | expect(camelCaseData['foo_gar']).toBe(3); 19 | }); 20 | it('deep option', () => { 21 | const data = { 22 | a_b: 1, 23 | a_c: { 24 | c_d: 1, 25 | c_e: { 26 | e_f: 1, 27 | }, 28 | }, 29 | }; 30 | const options = { 31 | deep: true, 32 | } as const; 33 | const camelCaseData = camelcaseKeys(data, options); 34 | expect(camelCaseData.aC.cE.eF).toBe(1); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/snakecase.spec.ts: -------------------------------------------------------------------------------- 1 | import { snakecaseKeys } from '@src/index'; 2 | 3 | describe('snakecase', () => { 4 | it('main', () => { 5 | const data = { 6 | fooBar: 2, 7 | }; 8 | const snakecaseData = snakecaseKeys(data); 9 | expect(snakecaseData['foo_bar']).toBe(2); 10 | }); 11 | it('exclude option', () => { 12 | const data = { 13 | fooBar: 2, 14 | fooGar: 3, 15 | }; 16 | const options = { exclude: ['foo_gar'] as const }; 17 | const snakecaseData = snakecaseKeys(data, options); 18 | expect(snakecaseData['foo_gar']).toBe(3); 19 | }); 20 | it('deep option', () => { 21 | const data = { 22 | aB: 1, 23 | aC: { 24 | cD: 1, 25 | cE: { 26 | eF: 1, 27 | }, 28 | }, 29 | }; 30 | const options = { 31 | deep: true, 32 | } as const; 33 | const snakecaseData = snakecaseKeys(data, options); 34 | expect(snakecaseData['a_c']['c_d']).toBe(1); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "strict": true, 5 | "lib": ["esnext"], 6 | "typeRoots": ["src/types"], 7 | "types": ["node", "@types/jest"], 8 | "target": "esnext", 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "noImplicitAny": true, 12 | "allowSyntheticDefaultImports": true, 13 | "esModuleInterop": true, 14 | "resolveJsonModule": true, 15 | "importHelpers": true, 16 | "noEmit": true, 17 | "allowJs": false, 18 | "paths": { 19 | "@src/*": ["src/*"], 20 | } 21 | }, 22 | "settings": { 23 | "import/resolver": { 24 | "node": { 25 | "extensions": [".ts","js"] 26 | } 27 | } 28 | }, 29 | "include": [ 30 | "src/*.ts", 31 | "tests/*.ts" 32 | ], 33 | "exclude": ["node_modules","example"] 34 | } 35 | --------------------------------------------------------------------------------