If you're searching for a mapped type with key remapping combination, we can achieve this by utilizing `key remapping` to retain only the desired properties:
type Key<T, F = any> = keyof {
[K in keyof T as T[K] extends F ? K : never]: T[K];
};
Let's run some tests:
interface Data {
items?: string[];
name: string;
}
// "name"
type Case1 = Key<Data, string>
To acquire an array type, we'll utilize `readonly unknown[]` as it encompasses mutable arrays:
// false
type Case1 = readonly number[] extends number[] ? true : false
// true
type Case2 = number[] extends readonly number[] ? true : false
Testing:
// type Case2 = never
type Case2 = Key<Data, readonly unknown[]>
Observe how `never` is returned? While `items` matches the type criterion, the issue lies in its optional nature. Optional objects are broader than required ones.
// false
type Case1 = { a?: string } extends { a: string } ? true : false;
// true
type Case2 = { a: string } extends { a?: string } ? true : false;
To address this, let's employ the built-in NonNullable utility type:
// string[] | undefined
type Case1 = Data['items']
// string[]
type Case2 = NonNullable<Data['items']>
Let's update `Key` and other types:
type Key<T, F = any> = keyof {
[K in keyof T as NonNullable<T[K]> extends F ? K : never]: T[K];
};
interface TextField<T> {
input: 'text';
key: Key<T, string>;
}
interface ArrayField<T> {
input: 'array';
key: Key<T, readonly unknown[]>;
}
Final testing:
// no error
const config: Field<Data>[] = [
{
input: 'text',
key: 'name',
},
{
input: 'array',
key: 'items',
},
];
// error because the items is not a string and name is not an array
const config2: Field<Data>[] = [
{
input: 'text',
key: 'items',
},
{
input: 'array',
key: 'name',
},
];
Playground Link