Maciej Sikora
Maciej Sikora

Reputation: 20162

TypeScript Conditional type extends object, available only properties

I want to achieve conditional behavior of type. When I put generic in form of object kind (array, object, tuple, record), really any composite type, then I want the type behave as a typeof field of this object, but when the given type is a primary, I want to be the same type.

type B<A> = A extends object ? A[keyof A] : A;
const tuple: [1, 2];
const element:B<typeof tuple> = 2; // # ISSUE - element can be also any method of array

const num: 1;
const numElment:B<typeof num> = 1;

Above code works but for composite B type enables me to assign also all method types from an Array type. My question is - how can I specify that I am interested only about things which are not functions. So only pure fields, as in example with the tuple, element should be only number.

I was trying also with extends {} or extends {[a: string | number]: any} but it was also not working, and even above snippet would break after that.

Upvotes: 3

Views: 26601

Answers (1)

jcalz
jcalz

Reputation: 330216

If you really want to exclude functions from a type union you can do it with the Exclude<T, U> utility type:

type ExcludeFunctionProps<T extends object> = Exclude<T[keyof T], Function>;

But I don't think this really does what you want:

type Hmm = ExcludeFunctionProps<["a", "b"]>; // "a" | "b" | 2

Here Hmm is "a" | "b" | 2. Why 2? Because arrays have a length property of a numeric type, and a pair tuple has that type as the numeric literal type 2. Unless you intend to include the tuple length, this is probably not the way to go.

type EvenWeirder = ExcludeFunctionProps<[string, () => void]>; // string | 2

And in the case where the tuple or object explicitly contains function-like values, this would also cut them out. Definitely seems like strange behavior to me.


Instead, I think that you're running into the issue where keyof T for an array type is still the full list of all array methods and properties even though you can map over array types without them. So what I'd try here is to make my own KeyofAfterMapping<T> which returns keyof T for non-array types, but only keeps the number index key for array types. Like this:

type KeyofAfterMapping<T> = Extract<
  keyof T,
  T extends any[] ? number : unknown
>;

Let's see what it does:

type KeyofPair = KeyofAfterMapping<["a", "b"]>; // number
type KeyofArray = KeyofAfterMapping<string[]>; // number
type KeyofObject = KeyofAfterMapping<{ a: string; b: number }>; // "a" | "b"

Now we can specify B:

type B<A> = A extends object ? A[KeyofAfterMapping<A>] : A;

And verify that it behaves as you expect:

type TryTupleAgain = B<["a", "b"]>; // "a" | "b"
type KeepsFunctions = B<Array<() => void>>; // () => void
type ObjectsAreStillFine = B<{ a: string; b: number }>; // string | number
type PrimitivesAreStillFine = B<string>; // string

Looks good. Okay, hope that helps. Good luck!

Link to code

Upvotes: 7

Related Questions