import { Arr, expectType, pipe } from 'ts-data-forge'; import { type IsNever, type ToString, type UnknownRecord } from 'ts-type-forge'; import { enumType } from '../enum/index.mjs'; import { undefinedType } from '../primitives/index.mjs'; import { type ShapeStructure, type Type, type TypeOf, hasRecordInternals, } from '../type.mjs'; export const keyof = ( recordType: Type, options?: Partial< Readonly<{ typeName: string; }> >, ): KeyofType => { if (!hasRecordInternals(recordType)) { throw new Error( `Expected a record type but received: ${recordType.typeName}`, ); } // eslint-disable-next-line total-functions/no-unsafe-type-assertion return pipe(getKeysFromStructure(recordType.shapeStructure)).map((keys) => Arr.isNonEmpty(keys) ? (enumType(keys, { typeName: options?.typeName ?? `keyof ${recordType.typeName}`, }) satisfies Type) : (undefinedType satisfies Type), ).value as KeyofType; }; const getKeysFromStructure = (structure: ShapeStructure): readonly string[] => { switch (structure.kind) { case 'simple': return Object.keys(structure.shape); case 'union': return getKeysHelper(structure.variants); case 'intersection': return getKeysHelper(structure.parts); } }; const getKeysHelper = ( structures: readonly ShapeStructure[], ): readonly string[] => { const mut_allKeys = new Set(); for (const part of structures) { const keys = getKeysFromStructure(part); for (const key of keys) { mut_allKeys.add(key); } } return Array.from(mut_allKeys); }; type KeyofType = IsNever extends true ? Type : Type>; // --- expectType assertions --- { type Base = Readonly<{ a: 0; b: 1; c: 2 }>; // keyof extracts string keys expectType>>, 'a' | 'b' | 'c'>('='); // keyof of empty record yields undefined type EmptyRecord = Readonly>; expectType>>, undefined>('='); // eslint-disable-next-line @typescript-eslint/no-empty-object-type expectType, Type>('='); expectType>, Type<'a' | 'b' | 'c'>>( '=', ); expectType< KeyofType>, Type<'x' | 'y' | 'z'> >('='); expectType< KeyofType>, Type<'same' | 'value'> >('='); expectType>, Type<'never'>>('='); }