Strange behaviors of Extend in Typescript

Strange behaviors of Extend in Typescript


6

I am currentlu working with the Effect Ts lib. In it I saw something I don’t understand:

export declare const typeSymbol: unique symbol
export type typeSymbol = typeof typeSymbol
type FilterIn<A> = A extends any ? typeSymbol extends keyof A ? A : never : never
type FilterOut<A> = A extends any ? typeSymbol extends keyof A ? never : A : never

I don’t understand when A extends any could be false?

I tried all possible types (even never, unknown, void, undefined, null…) in the typescript playground and could never find a type that does not extend any.

type t1 = undefined extends any ? 1 : 0 // Result: 1
type t2 = void extends any ? 1 : 0 // Result: 1
type t3 = null extends any ? 1 : 0 // Result: 1
type t4 = never extends any ? 1 : 0 // Result: 1
type t5 = unknown extends any ? 1 : 0 // Result: 1
type t6 = Function extends any ? 1 : 0 // Result: 1
type t7 = object extends any ? 1 : 0 // Result: 1

So what is the purpose of this branch?

I also tried this:

type t = never extends any ? 1 : 0 // Result: 1
type K<A> = A extends any ? 1 : 0
type u = K<never> // Result: never

How can u be never when one branch outputs 1 and the other outputs 0. I confess I am really confused now.

1 Answer
1


7

If A is a generic type parameter, then A extends XXX ? YYY : ZZZ is a distributive conditional type, meaning that any input type argument for A is first broken apart into its union members, then the conditional type is evaluated for each such member, and then the results are joined into a new union. So if F<T> is a distributive conditional type, then F<T0 | T1 | T2> will evaluate to F<T0> | F<T1> | F<T2>.

This is quite useful behavior. It lets you perform various operations on union types, such as filtering. In fact it is so useful that sometimes all you want to do is do the distribution and you’re not trying to actually check anything. That is, in A extends XXX ? YYY : ZZZ, you don’t care about XXX or ZZZ because all you care about is the true branch. All you’re trying to do is transform a non-distributive type G<T> into a distributive one F<T>. So you can write an "unconditional" conditional type like type F<T> = T extends any ? G<T> : never. Or type F<T> = T extends unknown ? G<T> : never. Or type F<T> = T extends T ? G<T> : never.

If you don’t know about distributive conditional types then this looks like a no-op, and that’s what’s confusing you about any.

The Extract and Exclude utility types are implemented this way, and so are the FilterIn and FilterOut types in your question.

So we get the following behavior:

type TestUnion = { [typeSymbol]: string } | { a: number }
type FilteredIn = FilterIn<TestUnion>;
// type FilteredIn = { [typeSymbol]: string; }
type FilteredOut = FilterOut<TestUnion>;
// type FilteredOut = { a: number }

The operation TypeSymbol extends keyof A ? A : never or TypeSymbol extends keyof A ? never : A wouldn’t work properly if unions in A were evaluated all at once, since the only possible outputs would be the entire A input, or never. But as we see above, these type functions split the input into its union members and treat each one independently. Thus FilteredIn contains just the union members with a key of type TypeSymbol, and FilteredOut contains just the union members without.

Playground link to code



Leave a Reply

Your email address will not be published. Required fields are marked *