Last active
March 3, 2024 16:31
-
-
Save heyzling/1849ecd7f6be95b8dd2183f8ca2a8282 to your computer and use it in GitHub Desktop.
Mapping Type, RecursiveRequired
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** Make type recursively Required. | |
* Source: https://gist.github.com/gomezcabo/dff1d95fd1eb354f686d6606a511d7da | |
* @typeParam T - Type of object to make recursively Required | |
* @typeParam K - fields which content should not be Required. | |
* Field themselfs still be Required, but their child fields will not. | |
* @typeParam OverrideRecordsWithArrays - flag. | |
* If true will override all Record<string,MyObj> in type to MyObj[] (i.e. array of objects) | |
*/ | |
export type RecursiveRequiredOld<T, K, RecordsToArrays extends boolean> = | |
// Т.к. это моя первая попытка сбацать Type Mapping штуковину, то ниже объясняю все построчно | |
// Поверхностно про Type Mapping в принципе: "метод", который конвертирует входной тип, согласно указаным "параметрам" | |
// В нашем случае на вход приходит сложный "многоэтажный" тип в котором часть свойств необязательна | |
// Нужно: | |
// 1. Сделать все поля обязательными (т.е. без знаков вопроса) РЕКУРСИВНО | |
// то есть нужно залезать в типы свойств и так далее - до самого дна | |
// 2. Игнорировать некоторые поля по их именам. Логика работает так, что с самого поля с именем "вопрос" уберется, | |
// а вот дальше "метод" не полезет | |
// 3. Опционально конвертировать словари типа Record<string,object> в массив типа object[] | |
// Это эксперементально сделано по фану, я не знаю насколько это реально будет удобно в коде, поэтому сделано опционально. | |
// Входные параметры: | |
// T - исходный тип, который нужно трансформировать. Может быть абсолютно любым, но тестирую я на своем KubernetesApplicationArgs | |
// K - поля, которые нужны исключить в виде Union Type. Пример: "fieldName1" | "fieldName2" | "fieldName3" | |
// RecordsToArrays - обычный boolean флаг. Если true, то все Record<string,object> конвертнутся в массивы вида object[] | |
// ==== ПОЕХАЛИ!!! ==== | |
// Все свойства должны быть Required. | |
// Исключениями будут будут только вложенные объекты исключения | |
// Теоретически, если нужно сделать также игнор полей верхнего уровня, | |
// то этот Requird нужно переместить внутрь фигурных скобок туда куда надо | |
Required<{ | |
// Это типа цикл. Буквально озанчает "для всех полей в типе T". | |
// В переменную P складывается "инфа о поле". | |
// В разных контекстах оно может означать "имя поля", а в других его "тип". | |
[P in keyof T]: | |
// == Общее по условиям | |
// далее идет куча последовательных условий. Conditional Types: https://www.typescriptlang.org/docs/handbook/2/conditional-types.html | |
// по сути тернарный оператор: (expression) ? (if true do this) : (if false to this) | |
// Обрати внимание, что они вложены в друга. Таким образом получается типа цепочки вложенных в друг друга if. | |
// Я постарался отделить их отступами для читаемости. То что после вопроса - ветка true, после двоеточия - ветка false. | |
// == P extends K | |
// Буквальное прочтение: Если поле P унаследовано от K -> тогда верни свойство как есть | |
// Напомню что в P - "инфа о поле". В данном случае, оно видимо интерпретируется как тип. | |
// K - это список полей, которые нужно игнорить. | |
P extends K | |
// T[P] - "взять тип поля P в классе T" | |
// Бизнес прочтение: Если имя поля встречается в K, то возвращаем как есть, и никак его не трансформируем. | |
// Именно этой строчкой реализуется фича №2. | |
? T[P] | |
// Далее, ветка false (поля нет в K) и вложенное условие | |
// Если тип поля - любой объект или undefined | |
// undefined - знак вопроса | |
// Знак вопроса у поля - это синтаксически сахар, который означает что поле может быть "такого типа" или undefined | |
: T[P] extends object | undefined | |
// Если это действительно объект, делаем еще проверки. | |
// Буквально: если свойство которое мы сейчас рассматриваем (T[P]) | |
// унаследовано от словаря типа Record<string,MyType> | |
// то положи тип MyType в переменную V - infer V | |
// | |
// Бизнес прочтение: это часть условия, которое превращает словари в массивы. Реализует фичу 3. | |
? T[P] extends Record<string,infer V extends object> | undefined | |
// Если флаг RecordsToArrays === true | |
? RecordsToArrays extends true | |
// Верни MyType[], но при этом MyType словно рекурсивно прогони через RecursiveRequired | |
// обрати внимание на скобочки в конце, | |
// это как раз превращение в массив рекурсивно проведенного типа | |
? RecursiveRequired<V,K,RecordsToArrays>[] | |
// Иначе оставь словарь как есть, но его вложенный тип рекурсивно прогони | |
// через RecursiveRequired | |
: Record<string, RecursiveRequired<V,K,RecordsToArrays>> | |
// Если это регулярный объект, не словарь, то прогони через RecursiveRequired | |
: RecursiveRequired<T[P], K, RecordsToArrays> | |
// если все условия провалилсь, то оставляем поле как есть | |
// так как у нас в самой первой строчке стоит обычный Required, | |
// то и это поле будет required, то есть без знака вопроса | |
: T[P] | |
}>; | |
// Легкая оптимизация | |
export type RecursiveRequired<T, K, RecordsToArrays extends boolean> = | |
{ | |
[P in keyof T]-?: // Убрал Reqruired заменил на -?, сразу в подсказках все стало читаемым | |
P extends K | |
? T[P] | |
: T[P] extends object | undefined | |
// добавил | undefined, не помню уже зачем, что-то важное пофиксил. | |
? T[P] extends Record<string,infer V extends object> | undefined | |
? RecordsToArrays extends true | |
? RecursiveRequired<V,K,RecordsToArrays>[] | |
: Record<string, RecursiveRequired<V,K,RecordsToArrays>> | |
: RecursiveRequired<T[P], K, RecordsToArrays> | |
: T[P] | |
}; | |
// ===== TESTS HELPERS ====== | |
// Сконструированы так, что если в типе выше что-то неправильно, компилятор будет подчеркивать красным. | |
// Оба методы в принципе показывают одно и тоже почти | |
/** Проверка, что тип B наследован от A. | |
* Он отлавливает не все что мне надо, поэтому надо обмазываться проверками. | |
* Source: https://stackoverflow.com/questions/56007865/testing-mapped-type | |
*/ | |
function checkExtends<A, B extends A>() { } | |
/** Проверить что объект соответствуют типу | |
* Source: https://stackoverflow.com/questions/59346116/is-there-a-way-to-check-for-type-equality-in-typescript | |
* Если объекта нет, а нужно проверить тип кидай в аргумент: {} as MyTypeToCheck | |
*/ | |
function checkEqual<T>(obj:T) {return undefined} | |
// ==== TEST INTERFACES ==== | |
// для тестов что условия работают использую эту заглушку, по ней в типе сразу видно куда что уехало. | |
interface Mock { } | |
interface TestPod { | |
name?: string | |
ports: Record<string, TestPort> | |
} | |
interface TestPort { | |
/** Порты */ | |
number: number | |
name?: string | |
} | |
interface TestVolume { | |
path: string | |
name?: string | |
} | |
interface TestWorkload { | |
/** Cловарь ключ значение */ | |
labels?: { [key: string]: string } | |
/** Обычный объект */ | |
volume?: TestVolume | |
/** Словарь типа Record */ | |
volumes: Record<string, TestVolume> | |
/** Словарь Pods, который содержт */ | |
pods: Record<string, TestPod> | |
//** Опциональные словарь подов (для бага в котором опциональные не прерващаются в массивы) */ | |
podsOptional?: Record<string, TestPod> | |
/** Кастомный тип указанный через обычное определение */ | |
c: { | |
d: string, | |
e?: string | |
}, | |
override?: { | |
one: string, | |
two?: string, | |
three?: string | |
} | |
} | |
// ==== TESTS ==== | |
// работают, кстати =). Это здорово | |
// checkExtends не отлавливает все случаи. Поэтому сопрвождай его проверками и всегда проверяй его достаточность | |
// == Опциональное свойство должно стать обязательным | |
declare const OptionalShouldBeRequiredVar: RecursiveRequired<{ a?: string }, '', false> | |
OptionalShouldBeRequiredVar.a.length | |
// == Свойства со сложным кастомным типом, все должн стать обязательным | |
// type RequiredCustomType = RecursiveRequired<{a?:{c?:string,b?:string}},'',false> | |
declare const RequiredCustomTypeVar: RecursiveRequired<{ a?: { c?: string, b?: string } }, '', false> | |
RequiredCustomTypeVar.a.c.length | |
RequiredCustomTypeVar.a.b.length | |
// == Record конвертируется в массив | |
type RecordToArray = RecursiveRequired<{ a: Record<string, TestVolume> }, '', true> | |
declare const RecordToArrayVar: RecordToArray | |
const RecordToArray_volumes: TestVolume[] = RecordToArrayVar.a // это действительно массив | |
RecordToArrayVar.a[0].name.length // это массив с Required элементами | |
// == Record опциональные конвертируется в массив | |
type OptionalRecordToArray = RecursiveRequired<{ a?: Record<string, TestVolume> }, '', true> | |
declare const OptionalRecordToArrayVar: OptionalRecordToArray | |
const OptionalRecordToArray_volumes: TestVolume[] = OptionalRecordToArrayVar.a // это действительно массив | |
OptionalRecordToArrayVar.a[0].name.length // это массив с Required элементами | |
checkEqual<Required<TestVolume>[]>(OptionalRecordToArrayVar.a) | |
// == Record НЕ конвертируется в массив | |
type DontConvert = RecursiveRequired<{ a: Record<string, TestVolume> }, '', false> | |
declare const DontConvertVar: DontConvert | |
const DontConvert_volumes: Record<string, TestVolume> = DontConvertVar.a // это действительно массив | |
DontConvertVar.a['sdfdsf'].name.length // это массив с Required элементами | |
checkEqual<Record<string, Required<TestVolume>>>(DontConvertVar.a) | |
// == Вложенный тип у Record - required | |
DontConvertVar.a['test'].name.length | |
// == работает игнорирование свойств | |
// вот здесь checkExtends хорошо отлавливает | |
type IgnoreProperty = RecursiveRequired<{ b?: { a?: string, b?: string } }, 'b', false> | |
checkExtends<IgnoreProperty, { b: { a?: string, b?: string } }>() | |
// ==== WORKLOAD | |
type WorkloadFullWithConversion = RecursiveRequired<TestWorkload, 'override', true> | |
declare const wl: WorkloadFullWithConversion | |
// declare const test: RecursiveRequired<Test,'',true> | |
// == c: должен быть полностью Required | |
wl.c.d.length | |
wl.c.e.length | |
// == labels: должен быть required | |
wl.labels['test'].length | |
// == volume: должен быть required | |
type VolumeType = typeof wl.volume | |
checkExtends<VolumeType, Required<TestVolume>>() | |
checkEqual<VolumeType>({} as Required<TestVolume>) | |
wl.volume.name | |
wl.volume.path | |
// == override: должен иметь в свойствах two и three вопросы | |
// проверка вызов свойств не очень надежна. Если вопрос исчезнет, код здесь не упадет | |
wl.override.two?.length | |
wl.override.three?.length | |
// поэтому дополнительно делаю checkExtends. | |
// Если в OverrideType пропадут знаки вопроса checkExtends покраснеет | |
type OverrideType = typeof wl.override | |
checkExtends<OverrideType, { one: string, two?: string, three?: string }>() | |
checkEqual<OverrideType>({} as { one: string, two?: string, three?: string }) | |
// == volumes: переделан в массив | |
const testVolumes: TestVolume[] = wl.volumes | |
// это массив элементов Required, то есть нельзя пихнуть не полностью определенный объект | |
wl.volumes.push({ name: "", path: "" }) | |
wl.volumes[0].name | |
wl.volumes[0].path | |
// == ports: порты вложенные в поды - обязательные (то есть работает рекурсивность) | |
wl.pods[0].ports[0].name | |
wl.pods[0].ports[0].number | |
checkEqual<Required<TestPort>[]>(wl.pods[0].ports) | |
// ==== Experiments ==== |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment