How to do a conditional generic type that has a value when it is undefined but not when it is undefined AND another thing

Viewed 1080

I have a type where I want to define an object key as optional with any If I do not give any value

type OptionalData<T> = T extends undefined ? { data?: any } : { data: T };

export type DataObject<T = any> = {
  item?: string | null;
} & OptionalData<T>;

It works ok until I set my generic as something | undefined

const data: DataObject<string | undefined> = { data: true }; // WRONG!
const data2: DataObject<string | number> = { data: true }; // CORRECT

since my generic can also extend undefined I get data as optional any

ipt

Is there a way to say "it is just optional any when it can JUST extend undefined" or so?

Playground

1 Answers

Problem

You have these types:

type OptionalData<T> = T extends undefined ? { data?: any } : { data: T };

export type DataObject<T = any> = {
  item?: string | null;
} & OptionalData<T>;

When you type a variable as DataObject with the type parameter being a union of a type (such as string) and undefined you expect the data property to be typed as the union type however it is typed as any:

const data: DataObject<string | undefined> = { data: ... }

// data.data should be typed as `string | undefined` however
// data.data is typed as `any`

What is happening?

TypeScript's documentation for conditional types is useful here. It reads:

Conditional types in which the checked type is a naked type parameter are called distributive conditional types. Distributive conditional types are automatically distributed over union types during instantiation. For example, an instantiation of T extends U ? X : Y with the type argument A | B | C for T is resolved as (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y).

– Documentation on distributive conditional types

So in your case it resolves to

| (string extends undefined ? { data?: any } : { data: T })
| (undefined extends undefined ? { data?: any } : { data: T })

The first conditional resolves to { data: T } and the second resolves to { data?: any }. I believe union types will resolve to the type that is the most generic so the type is eventually { data?: any }.

Solution

Here's OptionalData updated so that it works as expected:

type OptionalData<T> = [T] extends [undefined] ? { data?: any } : { data: T };

We just wrapped T and undefined with square brackets ([]), turning them into tuples (or technically monuple because it's only got one element). Here's the demo.

Why does this work?

Let's look back at the documentation for conditional types again, with some added emphasis on the important parts:

Conditional types in which the checked type is a naked type parameter are called distributive conditional types. Distributive conditional types are automatically distributed over union types during instantiation.

As we turned those naked types to tuples they are not naked anymore. And because they are no longer naked they are no longer distributed.

Related