We are building a mapped type that excludes properties of type Function
. Our approach has a problem: it also removes the optional (?
) decorator from mapped properties.
Here is a simplified reproduction of that behavior. NoOpMap1
behaves as we want it to, and NoOpMap2
has the problematic behavior.
type NoOpMap1<T> = { // Good. This one does not remove the ?
[K in keyof T]: T[K];
};
type Keys<T> = {
[K in keyof T]: K;
}[keyof T];
type NoOpMap2<T> = { // Problem. This one removes the ?
[K in Keys<T>]: T[K];
};
Demo
type SomeType = {
foo?: string,
}
// type SomeTypeNoOpMap1 = { foo?: string; }
type SomeTypeNoOpMap1 = NoOpMap1<SomeType>;
// type SomeTypeNoOpMap2 = { foo: string; }
type SomeTypeNoOpMap2 = NoOpMap2<SomeType>;
NoOpMap1
behaves as expected. It keeps the ?
decorater on the foo
property. NoOpMap2
removes it.
Why is NoOpMap2
removing the ?
decorator? How can we achieve a similar result without removing it?
Here is the complete type that we are trying to build:
type DataPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];
type DataPropertiesOnly<T> = {
[K in DataPropertyNames<T>]
: T[K] extends (string | number | boolean) ? T[K]
: T[K] extends (infer A)[] ? DataPropertiesOnly<A>[]
: DataPropertiesOnly<T[K]>;
};
As mentioned, the above type is responsible for removing properties of type Function
without removing the ?
decorator from remaining properties.
If you want to preserve the optional/readonly status of properties in a mapped type, you need to make sure that the compiler perceives the mapping as homomorphic. I know of two ways to do this.
One is for the mapping to be of the form {[K in keyof T]: ...}
where you are directly mapping over keyof T
for some T
, generic or concrete. You have to have something like in keyof
appearing directly in the type, or it won't count.
interface Foo {
optional?: string;
readonly viewonly: string;
}
type Homomorphic = { [K in keyof Foo]: 0 };
// type Homomorphic = {
// optional?: 0 | undefined;
// readonly viewonly: 0;
// }
type KeyOf<T> = keyof T
type NonHomomorphic = { [K in KeyOf<Foo>]: 0 };
// type NonHomomorphic = {
// optional: 0;
// viewonly: 0;
// }
The other way to do it is to map over a generic type parameter K
which has been constrained to keyof T
for another generic type parameter T
. So:
type GenericConstraint<T, K extends keyof T> = { [P in K]: 0 };
type ConstrainedHomomorphic = GenericConstraint<Foo, keyof Foo>;
// type ConstrainedHomomorphic = {
// optional?: 0 | undefined;
// readonly viewonly: 0;
// }
type OnlySomeKeysStillHomomorphic = GenericConstraint<Foo, "viewonly">;
// type OnlySomeKeysStillHomomorphic = {
// readonly viewonly: 0;
// }
This latter method was specifically added to get partial mapped types like Pick<T, K>
to be homomorphic. And it's this method you need to get your actual use case to work:
// unchanged
type DataPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];
// quick abort if T is a function or primitive
// otherwise pass to a homomorphic helper type
type DataPropertiesOnly<T> =
T extends Function ? never :
T extends object ? DPO<T, DataPropertyNames<T>> :
T
// homomorphic helper type
type DPO<T, KT extends keyof T> = {
[K in KT]
: T[K] extends (string | number | boolean) ? T[K]
: T[K] extends (infer A)[] ? DataPropertiesOnly<A>[]
: DataPropertiesOnly<T[K]>;
}
I think that will act the way you want.