Skip to content

Commit

Permalink
Export TypeScript type CamelCaseKeys (#90)
Browse files Browse the repository at this point in the history
  • Loading branch information
emmyakin committed Jun 6, 2022
1 parent 25bb862 commit 87c57ac
Show file tree
Hide file tree
Showing 2 changed files with 266 additions and 40 deletions.
18 changes: 9 additions & 9 deletions index.d.ts
Expand Up @@ -36,12 +36,12 @@ type AppendPath<S extends string, Last extends string> = S extends ''
/**
Convert keys of an object to camelcase strings.
*/
type CamelCaseKeys<
export type CamelCaseKeys<
T extends Record<string, any> | readonly any[],
Deep extends boolean,
IsPascalCase extends boolean,
Exclude extends readonly unknown[],
StopPaths extends readonly string[],
Deep extends boolean = false,
IsPascalCase extends boolean = false,
Exclude extends readonly unknown[] = EmptyTuple,
StopPaths extends readonly string[] = EmptyTuple,
Path extends string = ''
> = T extends readonly any[]
// Handle arrays or tuples.
Expand All @@ -57,11 +57,11 @@ type CamelCaseKeys<
: T extends Record<string, any>
// Handle objects.
? {
[P in keyof T & string as [IsInclude<Exclude, P>] extends [true]
[P in keyof T as [IsInclude<Exclude, P>] extends [true]
? P
: [IsPascalCase] extends [true]
? PascalCase<P>
: CamelCase<P>]: [IsInclude<StopPaths, AppendPath<Path, P>>] extends [
: CamelCase<P>]: [IsInclude<StopPaths, AppendPath<Path, P & string>>] extends [
true
]
? T[P]
Expand All @@ -72,7 +72,7 @@ type CamelCaseKeys<
IsPascalCase,
Exclude,
StopPaths,
AppendPath<Path, P>
AppendPath<Path, P & string>
>
: T[P];
}
Expand Down Expand Up @@ -189,4 +189,4 @@ WithDefault<Options['exclude'], EmptyTuple>,
WithDefault<Options['stopPaths'], EmptyTuple>
>;

export = camelcaseKeys;
export default camelcaseKeys;
288 changes: 257 additions & 31 deletions index.test-d.ts
@@ -1,46 +1,46 @@
import {expectType, expectAssignable} from 'tsd';
import camelcaseKeys = require('.');
import {expectType, expectAssignable, expectNotType} from 'tsd';
import type {CamelCaseKeys} from '.';
import camelcaseKeys from '.';

const fooBarObject = {'foo-bar': true};
const camelFooBarObject = camelcaseKeys(fooBarObject);
expectType<{fooBar: boolean}>(camelFooBarObject);
expectType<{ fooBar: boolean }>(camelFooBarObject);

const fooBarArray = [{'foo-bar': true}];
const camelFooBarArray = camelcaseKeys(fooBarArray);
expectType<Array<{fooBar: boolean}>>(camelFooBarArray);
expectType<Array<{ fooBar: boolean }>>(camelFooBarArray);

expectType<Array<{fooBar: boolean}>>(camelcaseKeys([{'foo-bar': true}]));
expectType<Array<{ fooBar: boolean }>>(camelcaseKeys([{'foo-bar': true}]));

expectType<string[]>(camelcaseKeys(['name 1', 'name 2']));

expectType<string[]>(camelcaseKeys(['name 1', 'name 2'], {deep: true}));

expectType<readonly [{fooBar: true}, {fooBaz: true}]>(
expectType<readonly [{ readonly fooBar: true }, { readonly fooBaz: true }]>(
camelcaseKeys([{'foo-bar': true}, {'foo-baz': true}] as const)
);

expectType<{fooBar: boolean}>(camelcaseKeys({'foo-bar': true}));
expectType<{fooBar: boolean}>(camelcaseKeys({'--foo-bar': true}));
expectType<{fooBar: boolean}>(camelcaseKeys({foo_bar: true}));
expectType<{fooBar: boolean}>(camelcaseKeys({'foo bar': true}));
expectType<{ fooBar: boolean }>(camelcaseKeys({'foo-bar': true}));
expectType<{ fooBar: boolean }>(camelcaseKeys({'--foo-bar': true}));
expectType<{ fooBar: boolean }>(camelcaseKeys({foo_bar: true}));
expectType<{ fooBar: boolean }>(camelcaseKeys({'foo bar': true}));

expectType<{fooBar: true}>(camelcaseKeys({'foo-bar': true} as const));
expectType<{fooBar: true}>(camelcaseKeys({'--foo-bar': true} as const));
expectType<{fooBar: true}>(camelcaseKeys({foo_bar: true} as const));
expectType<{fooBar: true}>(camelcaseKeys({'foo bar': true} as const));
expectType<{ readonly fooBar: true }>(camelcaseKeys({'foo-bar': true} as const));
expectType<{ readonly fooBar: true }>(camelcaseKeys({'--foo-bar': true} as const));
expectType<{ readonly fooBar: true }>(camelcaseKeys({foo_bar: true} as const));
expectType<{ readonly fooBar: true }>(camelcaseKeys({'foo bar': true} as const));

expectType<{fooBar: {fooBar: {fooBar: boolean}}}>(
camelcaseKeys(
{'foo-bar': {foo_bar: {'foo bar': true}}},
{deep: true}
)
expectType<{ fooBar: { fooBar: { fooBar: boolean } } }>(
camelcaseKeys({'foo-bar': {foo_bar: {'foo bar': true}}}, {deep: true})
);

interface ObjectOrUndefined {
foo_bar: {
foo_bar: {
foo_bar:
| {
foo_bar: boolean;
} | undefined;
}
| undefined;
};
}

Expand All @@ -52,43 +52,43 @@ const objectOrUndefined: ObjectOrUndefined = {
}
};

expectType<{fooBar: {fooBar: {fooBar: boolean} | undefined}}>(
expectType<{ fooBar: { fooBar: { fooBar: boolean } | undefined } }>(
camelcaseKeys(objectOrUndefined, {deep: true})
);

expectType<{FooBar: boolean}>(
expectType<{ FooBar: boolean }>(
camelcaseKeys({'foo-bar': true}, {pascalCase: true})
);
expectType<{FooBar: true}>(
expectType<{ readonly FooBar: true }>(
camelcaseKeys({'foo-bar': true} as const, {pascalCase: true})
);
expectType<{FooBar: boolean}>(
expectType<{ FooBar: boolean }>(
camelcaseKeys({'--foo-bar': true}, {pascalCase: true})
);
expectType<{FooBar: boolean}>(
expectType<{ FooBar: boolean }>(
camelcaseKeys({foo_bar: true}, {pascalCase: true})
);
expectType<{FooBar: boolean}>(
expectType<{ FooBar: boolean }>(
camelcaseKeys({'foo bar': true}, {pascalCase: true})
);
expectType<{FooBar: {FooBar: {FooBar: boolean}}}>(
expectType<{ FooBar: { FooBar: { FooBar: boolean } } }>(
camelcaseKeys(
{'foo-bar': {foo_bar: {'foo bar': true}}},
{deep: true, pascalCase: true}
)
);

expectType<{fooBar: boolean; foo_bar: true}>(
expectType<{ fooBar: boolean; foo_bar: true }>(
camelcaseKeys(
{'foo-bar': true, foo_bar: true},
{exclude: ['foo', 'foo_bar', /bar/] as const}
)
);

expectType<{fooBar: boolean}>(
expectType<{ fooBar: boolean }>(
camelcaseKeys({'foo-bar': true}, {stopPaths: ['foo']})
);
expectType<{topLevel: {fooBar: {'bar-baz': boolean}}; fooFoo: boolean}>(
expectType<{ topLevel: { fooBar: { 'bar-baz': boolean } }; fooFoo: boolean }>(
camelcaseKeys(
{'top-level': {'foo-bar': {'bar-baz': true}}, 'foo-foo': true},
{deep: true, stopPaths: ['top-level.foo-bar'] as const}
Expand Down Expand Up @@ -124,3 +124,229 @@ const objectWithTypeAlias = {

expectType<SomeTypeAlias>(camelcaseKeys(objectWithTypeAlias));
expectType<SomeTypeAlias[]>(camelcaseKeys([objectWithTypeAlias]));

// Using exported type
expectType<CamelCaseKeys<typeof fooBarArray>>(camelFooBarArray);

const arrayItems = [{fooBar: true}, {fooBaz: true}] as const;
expectType<CamelCaseKeys<typeof arrayItems>>(camelcaseKeys(arrayItems));

expectType<CamelCaseKeys<{ 'foo-bar': boolean }>>(
camelcaseKeys({'foo-bar': true})
);
expectType<CamelCaseKeys<{ '--foo-bar': boolean }>>(
camelcaseKeys({'--foo-bar': true})
);
expectType<CamelCaseKeys<{ foo_bar: boolean }>>(
camelcaseKeys({foo_bar: true})
);
expectType<CamelCaseKeys<{ 'foo bar': boolean }>>(
camelcaseKeys({'foo bar': true})
);

expectType<CamelCaseKeys<{ readonly 'foo-bar': true }>>(
camelcaseKeys({'foo-bar': true} as const)
);
expectType<CamelCaseKeys<{ readonly '--foo-bar': true }>>(
camelcaseKeys({'--foo-bar': true} as const)
);
expectType<CamelCaseKeys<{ readonly foo_bar: true }>>(
camelcaseKeys({foo_bar: true} as const)
);
expectType<CamelCaseKeys<{ readonly 'foo bar': true }>>(
camelcaseKeys({'foo bar': true} as const)
);

const nestedItem = {'foo-bar': {foo_bar: {'foo bar': true}}};
expectType<CamelCaseKeys<typeof nestedItem, true>>(
camelcaseKeys(nestedItem, {deep: true})
);

expectType<CamelCaseKeys<ObjectOrUndefined, true>>(
camelcaseKeys(objectOrUndefined, {deep: true})
);

expectType<CamelCaseKeys<{ 'foo-bar': boolean }, false, true>>(
camelcaseKeys({'foo-bar': true}, {pascalCase: true})
);
expectType<CamelCaseKeys<{ readonly 'foo-bar': true }, false, true>>(
camelcaseKeys({'foo-bar': true} as const, {pascalCase: true})
);
expectType<CamelCaseKeys<{ '--foo-bar': boolean }, false, true>>(
camelcaseKeys({'foo-bar': true}, {pascalCase: true})
);
expectType<CamelCaseKeys<{ foo_bar: boolean }, false, true>>(
camelcaseKeys({'foo-bar': true}, {pascalCase: true})
);
expectType<CamelCaseKeys<{ 'foo bar': boolean }, false, true>>(
camelcaseKeys({'foo-bar': true}, {pascalCase: true})
);
expectType<CamelCaseKeys<typeof nestedItem, true, true>>(
camelcaseKeys(nestedItem, {deep: true, pascalCase: true})
);

const data = {'foo-bar': true, foo_bar: true};
const exclude = ['foo', 'foo_bar', /bar/] as const;

expectType<CamelCaseKeys<typeof data, false, false, typeof exclude>>(
camelcaseKeys(data, {exclude})
);

const nonNestedWithStopPathData = {'foo-bar': true, foo_bar: true};
expectType<
CamelCaseKeys<typeof nonNestedWithStopPathData, false, false, ['foo']>
>(camelcaseKeys({'foo-bar': true}, {stopPaths: ['foo']}));
const nestedWithStopPathData = {
'top-level': {'foo-bar': {'bar-baz': true}},
'foo-foo': true
};
const stopPaths = ['top-level.foo-bar'] as const;
expectType<
CamelCaseKeys<
typeof nestedWithStopPathData,
true,
false,
// eslint-disable-next-line @typescript-eslint/ban-types
[],
typeof stopPaths
>
>(camelcaseKeys(nestedWithStopPathData, {deep: true, stopPaths}));

expectAssignable<CamelCaseKeys<Record<string, string>>>(
camelcaseKeys({} as Record<string, string>)
);

expectAssignable<CamelCaseKeys<Record<string, string>, true>>(
camelcaseKeys({} as Record<string, string>, {deep: true})
);

expectType<CamelCaseKeys<SomeObject>>(camelcaseKeys(someObject));
expectType<CamelCaseKeys<SomeObject[]>>(camelcaseKeys([someObject]));

expectType<CamelCaseKeys<SomeTypeAlias>>(camelcaseKeys(objectWithTypeAlias));
expectType<CamelCaseKeys<SomeTypeAlias[]>>(
camelcaseKeys([objectWithTypeAlias])
);

// Verify exported type `CamelcaseKeys`
// Mapping types and retaining properties of keys
// https://github.com/microsoft/TypeScript/issues/13224

type ObjectDataType = {
foo_bar?: string;
bar_baz?: string;
baz: string;
};
type InvalidConvertedObjectDataType = {
fooBar: string;
barBaz: string;
baz: string;
};
type ConvertedObjectDataType = {
fooBar?: string;
barBaz?: string;
baz: string;
};

const objectInputData: ObjectDataType = {
foo_bar: 'foo_bar',
baz: 'baz'
};
expectType<ConvertedObjectDataType>(camelcaseKeys(objectInputData));
expectNotType<InvalidConvertedObjectDataType>(camelcaseKeys(objectInputData));

// Array
type ArrayDataType = ObjectDataType[];

const arrayInputData: ArrayDataType = [
{
foo_bar: 'foo_bar',
baz: 'baz'
}
];
expectType<ConvertedObjectDataType[]>(camelcaseKeys(arrayInputData));
expectNotType<InvalidConvertedObjectDataType[]>(camelcaseKeys(arrayInputData));

// Deep
type DeepObjectType = {
foo_bar?: string;
bar_baz?: string;
baz: string;
first_level: {
foo_bar?: string;
bar_baz?: string;
second_level: {
foo_bar: string;
bar_baz?: string;
};
};
};
type InvalidConvertedDeepObjectDataType = {
fooBar?: string;
barBaz?: string;
baz: string;
first_level?: {
fooBar?: string;
barBaz?: string;
second_level?: {
fooBar: string;
barBaz?: string;
};
};
};
type ConvertedDeepObjectDataType = {
fooBar?: string;
barBaz?: string;
baz: string;
firstLevel: {
foo_bar?: string;
bar_baz?: string;
second_level: {
foo_bar: string;
bar_baz?: string;
};
};
};
const deepInputData: DeepObjectType = {
foo_bar: 'foo_bar',
baz: 'baz',
first_level: {
bar_baz: 'bar_baz',
second_level: {
foo_bar: 'foo_bar'
}
}
};
expectType<ConvertedDeepObjectDataType>(
camelcaseKeys(deepInputData, {deep: false})
);
expectNotType<InvalidConvertedDeepObjectDataType>(
camelcaseKeys(deepInputData, {deep: false})
);

// Exclude
type InvalidConvertedExcludeObjectDataType = {
foo_bar?: string;
bar_baz?: string;
baz: string;
};
type ConvertedExcludeObjectDataType = {
foo_bar?: string;
barBaz?: string;
baz: string;
};
const excludeInputData: ObjectDataType = {
foo_bar: 'foo_bar',
bar_baz: 'bar_baz',
baz: 'baz'
};
expectType<ConvertedExcludeObjectDataType>(
camelcaseKeys(excludeInputData, {
exclude
})
);
expectNotType<InvalidConvertedExcludeObjectDataType>(
camelcaseKeys(excludeInputData, {
exclude
})
);

0 comments on commit 87c57ac

Please sign in to comment.