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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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] | ||
}; |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -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 | ||||||
*/ | ||||||
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`. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
@param {{ cascadeOptionality?: boolean }} Options The options to use when flattening | ||||||
@example | ||||||
``` | ||||||
import type { DeepFlattenRecord } from 'type-fest'; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
type ExampleType = { | ||||||
arrayKey: string[]; | ||||||
objectKey: { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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]; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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[] | ||
>; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.