Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add DeepFlattenRecord type (and helpers) #571

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions index.d.ts
Expand Up @@ -83,6 +83,10 @@ export type {
IsBooleanLiteral,
IsSymbolLiteral,
} from './source/is-literal';
export type {RecordValuesOf} from './source/record-values-of';
export type {NonRecordKeysOf} from './source/non-record-keys-of';
export type {CascadeOptional} from './source/cascade-optional';
export type {DeepFlattenRecord} from './source/deep-flatten-record';

// Template literal types
export type {CamelCase} from './source/camel-case';
Expand Down
4 changes: 4 additions & 0 deletions readme.md
Expand Up @@ -178,6 +178,10 @@ Click the type names for complete docs.
- [`IsNumericLiteral`](source/is-literal.d.ts) - Returns a boolean for whether the given type is a `number` or `bigint` [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types).
- [`IsBooleanLiteral`](source/is-literal.d.ts) - Returns a boolean for whether the given type is a `true` or `false` [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types).
- [`IsSymbolLiteral`](source/is-literal.d.ts) - Returns a boolean for whether the given type is a `symbol` [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types).
- [`CascadeOptional`](source/cascade-optional.d.ts) - Creates a type that applies `Partial` to all fields of all optional keys that are Records.
- [`NonRecordKeysOf`](source/non-record-keys-of.d.ts) - Get all keys from the given type that are not Records.
- [`RecordValuesOf`](source/record-values-of.d.ts) - Get all values from the given type that are Records.
- [`DeepFlattenRecord`](source/deep-flatten-record.d.ts) - Create a type that flattens all nested Records into a single level.

### JSON

Expand Down
53 changes: 53 additions & 0 deletions source/cascade-optional.d.ts
@@ -0,0 +1,53 @@
import type {OptionalKeysOf} from './optional-keys-of';

/**
Creates a type that applies `Partial` keys that are optional and are of type Record. This applies one level deep. This is useful when you want to create an API whose behavior depends on the presence or absence of required fields.
This is also a helper type for `DeepFlattenRecord`.
@see DeepFlattenRecord

@example
```
import type { CascadeOptional } from 'type-fest';
type ExampleType = {
arrayKey: string[];
objectKey: {
nestedTwoArrayKey: string[]
nestedTwoObjectKey?: {
nestedThreeNumberKey: number;
}
nestedTwoStringKey: string;
nestedTwoFunctionKey: () => void;
}
stringKey: string;
numberKey: number;
functionKey: () => void;
}

type ExampleTypeCascadeOptional = CascadeOptional<ExampleType>;
//=> {
// arrayKey: string[];
// objectKey: {
// nestedTwoArrayKey: string[];
// nestedTwoObjectKey?: {
// nestedThreeNumberKey: number;
// };
// nestedTwoStringKey: string;
// nestedTwoFunctionKey: () => void;
// };
// stringKey: string;
// numberKey: number;
// functionKey: () => void;
// }
*/
export type CascadeOptional<BaseType extends Record<string | number | symbol, unknown>> = {
// Check that the keys of the record are one of the optional keys.
[Key in keyof BaseType]: Key extends OptionalKeysOf<BaseType>
// If the key is optional, check if the value is a Record.
? NonNullable<BaseType[Key]> extends Record<string | number | symbol, unknown>
// If the value is a Record (and therefore is an optional record), return the value with `Partial` applied.
? Partial<BaseType[Key]>
// If the value is not a Record, return the value.
: BaseType[Key]
// If the key is not optional return the value.
: BaseType[Key]
};
73 changes: 73 additions & 0 deletions source/deep-flatten-record.d.ts
@@ -0,0 +1,73 @@
import type {NonRecordKeysOf} from './non-record-keys-of';
import type {UnionToIntersection} from './union-to-intersection';
import type {CascadeOptional} from './cascade-optional';
import type {RecordValuesOf} from './record-values-of';

/**
DeepFlattenRecord options

@see DeepFlattenRecord
*/
export type DeepFlattenOptions = {
/**
Whether to cascade optionality on an object (e.g. if a parent is optional, all children will be as well regardless of their original optionality)
@default true
*/
Comment on lines +12 to +15
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/**
Whether to cascade optionality on an object (e.g. if a parent is optional, all children will be as well regardless of their original optionality)
@default true
*/
/**
Whether to cascade optionality on an object (e.g. if a parent is optional, all children will be as well regardless of their original optionality)
@default true
*/

readonly cascadeOptionality?: boolean;
};

/**
Deeply flattens a Record type recursively. If a key is an object, it will be flattened into the parent object.
By default, if a key containing an object is optional,the object will be flattened into the parent object and all
it's keys will be optional. This can be disabled by setting `cascadeOptionality` in `DeepFlattenOptions` to `false`.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't hard-wrap.


@see DeepFlattenOptions

This is useful when you have a nested object that you want to use as a form or table schema, but you want to flatten it into a single object.

@param BaseType The type to flatten
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@param BaseType The type to flatten
@param BaseType - The type to flatten.

@param {{ cascadeOptionality?: boolean }} Options The options to use when flattening
@example
```
import type { DeepFlattenRecord } from 'type-fest';
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import type { DeepFlattenRecord } from 'type-fest';
import type {DeepFlattenRecord} from 'type-fest';


type ExampleType = {
arrayKey: string[];
objectKey: {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use tab-indentation.

nestedTwoArrayKey: string[]
nestedTwoObjectKey?: {
nestedThreeNumberKey: number;
}
nestedTwoStringKey: string;
nestedTwoFunctionKey: () => void;
}
stringKey: string;
numberKey: number;
functionKey: () => void;
}

type ExampleTypeFlattened = DeepFlattenRecord<ExampleType>
//=>{
// arrayKey: string[];
// nestedTwoArrayKey: string[];
// nestedThreeNumberKey?: number;
// nestedTwoStringKey: string;
// nestedTwoFunctionKey: () => void;
// stringKey: string;
// numberKey: number;
// functionKey: () => void;
// }
```
*/
export type DeepFlattenRecord<BaseType, Options extends DeepFlattenOptions = {cascadeOptionality: true}> =
// Check if the type is a Record
BaseType extends Record<string | number | symbol, unknown>
// If it is, start by grabbing all the keys that are not Records
? Pick<BaseType, NonRecordKeysOf<BaseType>>
// Then, grab all the keys that are Records and flatten them
& UnionToIntersection<DeepFlattenRecord<RecordValuesOf<
// If cascadeOptionality is true and the BaseType is optional, make all the child keys optional
Options['cascadeOptionality'] extends true ? CascadeOptional<BaseType> : BaseType
>>>
// If it's not a Record, just 'return'
: never;
36 changes: 36 additions & 0 deletions source/non-record-keys-of.d.ts
@@ -0,0 +1,36 @@
/**
Get the keys of a type that are not records. This is a helper type for `DeepFlattenRecord`.
@see DeepFlattenRecord

@example
'''
import type { NonRecordKeysOf } from 'type-fest';
type ExampleType = {
arrayKey: string[];
objectKey: {
nestedTwoArrayKey: string[]
nestedTwoObjectKey?: {
nestedThreeNumberKey: number;
}
nestedTwoStringKey: string;
nestedTwoFunctionKey: () => void;
}
stringKey: string;
numberKey: number;
functionKey: () => void;
}

type ExampleTypeNonRecordKeys = NonRecordKeysOf<ExampleType>;
//=> 'arrayKey' | 'objectKey' | 'stringKey' | 'numberKey' | 'functionKey'
'''
*/
export type NonRecordKeysOf<BaseType extends Record<string | number | symbol, unknown>> = {
[Key in keyof BaseType]: BaseType[Key] extends unknown[]
// Include arrays
? Key
: BaseType[Key] extends Function
// Include functions
? Key
// Check if the value is a Record, return never if it is, otherwise return the key. (Exclude Records)
: NonNullable<BaseType[Key]> extends Record<string | number | symbol, unknown> ? never : Key
}[keyof BaseType];
40 changes: 40 additions & 0 deletions source/record-values-of.d.ts
@@ -0,0 +1,40 @@
/**
Extracts the values of a Record that are Records. This is a helper type for `DeepFlattenRecord`.
@see DeepFlattenRecord

@example
```
import type { RecordValuesOf } from 'type-fest';

type ExampleType = {
arrayKey: string[];
objectKey: {
nestedTwoArrayKey: string[]
nestedTwoObjectKey?: {
nestedThreeNumberKey: number;
}
nestedTwoStringKey: string;
nestedTwoFunctionKey: () => void;
}
stringKey: string;
numberKey: number;
functionKey: () => void;
}

type ExampleTypeValues = ObjectValuesOf<ExampleType>;
//=> {
// nestedTwoArrayKey: string[];
// nestedTwoObjectKey?: {
// nestedThreeNumberKey: number;
// };
// nestedTwoStringKey: string;
// nestedTwoFunctionKey: () => void;
// }
```
*/
export type RecordValuesOf<BaseType extends Record<string | number | symbol, unknown>> =
Exclude<
// 1. Extract the values of the record that are type Record.
Extract<BaseType[keyof BaseType], Record<string | number | symbol, unknown>>,
any[]
>;
55 changes: 55 additions & 0 deletions test-d/cascade-optional.ts
@@ -0,0 +1,55 @@
import {expectType} from 'tsd';
import type {CascadeOptional} from '../index';

type Test1 = {
objectKey: {
nestedArrayKey: string[];
};
};

type ExpectedTest1 = {
objectKey: {
nestedArrayKey: string[];
};
};

declare const testValue1: ExpectedTest1;
expectType<CascadeOptional<Test1>>(testValue1);

type Test2 = {
objectKey: {
nestedObjectKey?: {
nestedKey: unknown;
};
};
};

type ExpectedTest2 = {
objectKey: {
nestedObjectKey?: {
nestedKey: unknown;
};
};
};

declare const testValue2: ExpectedTest2;
expectType<CascadeOptional<Test2>>(testValue2);

type Test3 = {
objectKeyOptional?: {
nestedKeyNotOriginallyOptional: {
deeplyNestedKey: unknown;
};
};
};

type ExpectedTest3 = {
objectKeyOptional?: {
nestedKeyNotOriginallyOptional?: {
deeplyNestedKey: unknown;
};
};
};

declare const testValue3: ExpectedTest3;
expectType<CascadeOptional<Test3>>(testValue3);
45 changes: 45 additions & 0 deletions test-d/deep-flatten-objects.ts
@@ -0,0 +1,45 @@
import {expectAssignable} from 'tsd';
import type {DeepFlattenRecord} from '../index';

type Test1 = {
arrayKey: string[];
objectKey: {
nestedTwoArrayKey: string[];
nestedTwoObjectKey?: {
nestedThreeNumberKey: number;
};
nestedTwoStringKey: string;
nestedTwoFunctionKey: () => void;
};
stringKey: string;
numberKey: number;
functionKey: () => void;
};

type ExpectedTest1Cascade = {
arrayKey: string[];
nestedTwoArrayKey: string[];
nestedThreeNumberKey?: number;
nestedTwoStringKey: string;
nestedTwoFunctionKey: () => void;
stringKey: string;
numberKey: number;
functionKey: () => void;
};

type ExpectedTest1NoCascade = {
arrayKey: string[];
nestedTwoArrayKey: string[];
nestedThreeNumberKey: number;
nestedTwoStringKey: string;
nestedTwoFunctionKey: () => void;
stringKey: string;
numberKey: number;
functionKey: () => void;
};

declare const actualValueCascadeOptionality: ExpectedTest1Cascade;
declare const actualValueNoCascade: ExpectedTest1NoCascade;

expectAssignable<DeepFlattenRecord<Test1>>(actualValueCascadeOptionality);
expectAssignable<DeepFlattenRecord<Test1, {cascadeOptionality: false}>>(actualValueNoCascade);
23 changes: 23 additions & 0 deletions test-d/non-record-keys-of.ts
@@ -0,0 +1,23 @@
import {expectType} from 'tsd';
import type {NonRecordKeysOf} from '../index';

type ExampleType = {
arrayKey: string[];
objectKey: {
nestedTwoArrayKey: string[];
nestedTwoObjectKey?: {
nestedThreeNumberKey: number;
};
nestedTwoStringKey: string;
nestedTwoFunctionKey: () => void;
};
stringKey: string;
numberKey: number;
functionKey: () => void;
};

type ExpectedType = 'arrayKey' | 'stringKey' | 'numberKey' | 'functionKey';

declare const actualValue: ExpectedType;

expectType<NonRecordKeysOf<ExampleType>>(actualValue);
30 changes: 30 additions & 0 deletions test-d/record-values-of.ts
@@ -0,0 +1,30 @@
import {expectType} from 'tsd';
import type {RecordValuesOf} from '../index';

type ExampleType = {
arrayKey: string[];
objectKey: {
nestedTwoArrayKey: string[];
nestedTwoObjectKey?: {
nestedThreeNumberKey: number;
};
nestedTwoStringKey: string;
nestedTwoFunctionKey: () => void;
};
stringKey: string;
numberKey: number;
functionKey: () => void;
};

type ExpectedType = {
nestedTwoArrayKey: string[];
nestedTwoObjectKey?: {
nestedThreeNumberKey: number;
};
nestedTwoStringKey: string;
nestedTwoFunctionKey: () => void;
};

declare const actualValue: ExpectedType;

expectType<RecordValuesOf<ExampleType>>(actualValue);