Skip to content

Instantly share code, notes, and snippets.

@heyzling
Last active March 3, 2024 16:31
Show Gist options
  • Save heyzling/1849ecd7f6be95b8dd2183f8ca2a8282 to your computer and use it in GitHub Desktop.
Save heyzling/1849ecd7f6be95b8dd2183f8ca2a8282 to your computer and use it in GitHub Desktop.
Mapping Type, RecursiveRequired
/** 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