I'm trying to create a new mapped type from an existing type. I'm looking to replace all nullable properties with a string
type. Basic idea is to have the mapped type replace all nullable property types in sub-records with a string
type. If the sub-record doesn't include any nullable types the sub-record itself is excluded from the mapped type. Also any property that is not an object is excluded.
interface OriginalType {
foo: {
bar: string | null;
baz: number | null;
};
bar: {
qux: string;
quz: boolean | null;
wobble: string | null;
};
baz: {
grault: string;
garply: number;
flob: boolean;
};
version: number;
}
interface ExpectedType {
foo: {
bar: string;
baz: string;
};
bar: {
quz: string;
wobble: string;
};
}
I currently have written this mapped type so far:
type RetainNullablesAsString<T> = {
[C in keyof T]: T[C] extends object ? {
[K in keyof T[C]]: null extends T[C][K] ? string : never;
}: never;
}
If testing it I get an error:
Property 'qux' is missing in type '{ quz: string; wobble: string; }' but required in type '{ qux: never; quz: string; wobble: string; }'.
I intend the qux
property to be excluded from the type as it's not nullable but I don't know how to do that - I currently set it to never
in the mapped type. The Pick utility type would't be of help here I think as I don't want to hardcode any properties to the mapped type. Also the baz
record should be excluded as all of it's child properties are not nullable. Moreover I don't know if it's even possible to create a mapped type like this but would be glad if it was.
interface OriginalType {
foo: {
bar: string | null;
baz: number | null;
};
bar: {
qux: string;
quz: boolean | null;
wobble: string | null;
};
baz: {
grault: string;
garply: number;
flob: boolean;
};
version: number;
}
const sourceRecord: OriginalType = {
foo: {
bar: 'bar',
baz: 0,
},
bar: {
qux: 'qux',
quz: false,
wobble: null,
},
baz: {
grault: 'grault',
garply: 1,
flob: true,
},
version: 1,
}
//interface ExpectedType {
// foo: {
// bar: string;
// baz: string;
// };
// bar: {
// quz: string;
// wobble: string;
// };
//}
type RetainNullablesAsString<T> = {
[C in keyof T]: T[C] extends object ? {
[K in keyof T[C]]: null extends T[C][K] ? string : never;
}: never;
}
const sourceRecordMetaData: RetainNullablesAsString<OriginalType> = {
foo: {
bar: 'bar-meta',
baz: 'baz-meta',
},
bar: {
quz: 'quz-meta',
wobble: 'wobble-meta',
},
}
interface OriginalType {
foo: {
bar: string | null;
baz: number | null;
};
bar: {
qux: string;
quz: boolean | null;
wobble: string | null;
};
baz: {
grault: string;
garply: number;
flob: boolean;
};
version: number;
}
type IsNullableProperty<K extends keyof T, T> = null extends T[K] ? K : never;
// Filtering out non nullable properties and convert rest to strings
type ConvertSubRecord<T> = {
[K in keyof T as IsNullableProperty<K, T>]: string;
};
type IsEmptyObject<T> = keyof T extends never ? never : T;
// It transform property keys for non records and records that are converted into empty objects into never
type IsNotEmptySubRecord<K extends keyof T, T> = T[K] extends object
? keyof ConvertSubRecord<T[K]> extends never
? never
: K
: never;
type Convert<T> = {
// Filter out with IsNotEmptySubRecord helper and convert subrecords with ConvertSubRecord
[K in keyof T as IsNotEmptySubRecord<K, T>]: ConvertSubRecord<T[K]>;
};
interface ExpectedType {
foo: {
bar: string;
baz: string;
};
bar: {
quz: string;
wobble: string;
};
}
const res: Convert<OriginalType> = {
bar: {
quz: "",
wobble: ""
},
foo: {
bar: "",
baz: "",
},
}