Get Array Element Type Safely

5 min read View on GitHub

When working with types that are out of your control like API responses, GraphQL codegen, or third-party libraries, you might need the type of an element inside an array, not the array itself. TypeScript makes this easy in the simple cases, but it gets trickier when the array lives inside another type and might be optional.

Some time back, I helped a teammate with this situation. This post is a recollection of that solution. We’ll start with the obvious, run into rough edges, and end with a reusable strict-safe helper.

Step 0: The Obvious Case

If we have an array property and we know it’s always there, we can use T[number] to get the element type.

interface
interface Store
Store
{
Store.orders: {
id: string;
total: number;
}[]
orders
: {
id: string
id
: string;
total: number
total
: number }[];
}
type Order =
interface Store
Store
["orders"][number];
type Order = {
id: string;
total: number;
}

This works because Store["orders"] is an array, and indexing an array type with number gives us the element type. In other words, this is how you ask TypeScript: “give me one of its elements.”

Simple and readable. Perfect for required array properties. But what if we want a reusable version for raw arrays?

Step 1: Generalizing to Raw Arrays

Let’s create a utility type that extracts the element type from any array:

type
type ElementOf<T> = T extends readonly unknown[] ? T[number] : never
ElementOf
<
function (type parameter) T in type ElementOf<T>
T
> =
function (type parameter) T in type ElementOf<T>
T
extends readonly unknown[] ?
function (type parameter) T in type ElementOf<T>
T
[number] : never;
type Num =
type ElementOf<T> = T extends readonly unknown[] ? T[number] : never
ElementOf
<number[]>;
type Num = number
type Tuple =
type ElementOf<T> = T extends readonly unknown[] ? T[number] : never
ElementOf
<[1, 2, 3]>;
type Tuple = 3 | 1 | 2
interface
interface Store
Store
{
Store.orders: {
id: string;
total: number;
}[]
orders
: {
id: string
id
: string;
total: number
total
: number }[];
}
type Order =
type ElementOf<T> = T extends readonly unknown[] ? T[number] : never
ElementOf
<
interface Store
Store
["orders"]>;
type Order = {
id: string;
total: number;
}
Note

We use readonly unknown[] instead of unknown[] because it matches both mutable and readonly arrays—TypeScript considers T[] to be assignable to readonly T[].

This works for plain arrays and tuples, but what about when the array is a property of another type?

Step 2: Targeting Object Properties

We can add a second generic parameter for the property key:

interface
interface Store
Store
{
Store.orders: {
id: string;
total: number;
}[]
orders
: {
id: string
id
: string;
total: number
total
: number }[];
}
type
type ElementOf<T, K extends keyof T> = T[K] extends readonly unknown[] ? T[K][number] : never
ElementOf
<
function (type parameter) T in type ElementOf<T, K extends keyof T>
T
,
function (type parameter) K in type ElementOf<T, K extends keyof T>
K
extends keyof
function (type parameter) T in type ElementOf<T, K extends keyof T>
T
> =
function (type parameter) T in type ElementOf<T, K extends keyof T>
T
[
function (type parameter) K in type ElementOf<T, K extends keyof T>
K
] extends readonly unknown[]
?
function (type parameter) T in type ElementOf<T, K extends keyof T>
T
[
function (type parameter) K in type ElementOf<T, K extends keyof T>
K
][number]
: never;
type Order =
type ElementOf<T, K extends keyof T> = T[K] extends readonly unknown[] ? T[K][number] : never
ElementOf
<
interface Store
Store
, "orders">;
type Order = {
id: string;
total: number;
}

Now we can say: “give me the element type of the orders property on Store.”

However, this breaks down with strictNullChecks enabled if the property is optional. Without strict null checks, optional properties don’t include undefined in their type, so the approach above would still work. But with strict mode (which you should be using), we hit a problem.

interface
interface Store
Store
{
Store.orders?: {
id: string;
total: number;
}[]
orders
?: {
id: string
id
: string;
total: number
total
: number }[];
}
type
type ElementOf<T, K extends keyof T> = T[K] extends readonly unknown[] ? T[K][number] : never
ElementOf
<
function (type parameter) T in type ElementOf<T, K extends keyof T>
T
,
function (type parameter) K in type ElementOf<T, K extends keyof T>
K
extends keyof
function (type parameter) T in type ElementOf<T, K extends keyof T>
T
> =
function (type parameter) T in type ElementOf<T, K extends keyof T>
T
[
function (type parameter) K in type ElementOf<T, K extends keyof T>
K
] extends readonly unknown[]
?
function (type parameter) T in type ElementOf<T, K extends keyof T>
T
[
function (type parameter) K in type ElementOf<T, K extends keyof T>
K
][number]
: never;
Warning: Order is not what we expect.
type Order =
type ElementOf<T, K extends keyof T> = T[K] extends readonly unknown[] ? T[K][number] : never
ElementOf
<
interface Store
Store
, "orders">;
type Order = never

As you can see, Order is never because T[K] is { id: string; total: number }[] | undefined, and that union does not extend readonly unknown[].

Let’s fix that.

Step 3: Handling Optional Properties (Strict-Safe)

To improve our Step 2 solution, we need to extract only the array type from the union before indexing. But we also want to keep our utility working for raw arrays from Step 1. Here’s how we do both:

interface
interface Store
Store
{
Store.orders?: {
id: string;
total: number;
}[]
orders
?: {
id: string
id
: string;
total: number
total
: number }[];
}
type
type ElementOf<T, K extends keyof T = never> = [K] extends [never] ? T extends readonly unknown[] ? T[number] : never : Extract<T[K], readonly unknown[]>[number]
ElementOf
<
function (type parameter) T in type ElementOf<T, K extends keyof T = never>
T
,
function (type parameter) K in type ElementOf<T, K extends keyof T = never>
K
extends keyof
function (type parameter) T in type ElementOf<T, K extends keyof T = never>
T
= never> = [
function (type parameter) K in type ElementOf<T, K extends keyof T = never>
K
] extends [never]
?
function (type parameter) T in type ElementOf<T, K extends keyof T = never>
T
extends readonly unknown[]
?
function (type parameter) T in type ElementOf<T, K extends keyof T = never>
T
[number]
: never
:
type Extract<T, U> = T extends U ? T : never

Extract from T those types that are assignable to U

Extract
<
function (type parameter) T in type ElementOf<T, K extends keyof T = never>
T
[
function (type parameter) K in type ElementOf<T, K extends keyof T = never>
K
], readonly unknown[]>[number];
Message: Order is what we expect.
type Order =
type ElementOf<T, K extends keyof T = never> = [K] extends [never] ? T extends readonly unknown[] ? T[number] : never : Extract<T[K], readonly unknown[]>[number]
ElementOf
<
interface Store
Store
, "orders">;
type Order = {
id: string;
total: number;
}

How This Works

The mode switch: [K] extends [never] checks whether a key was provided:

  • No key passed (K defaults to never): Use the raw array logic from Step 1

    type Num =
    type ElementOf<T, K extends keyof T = never> = [K] extends [never] ? T extends readonly unknown[] ? T[number] : never : Extract<T[K], readonly unknown[]>[number]
    ElementOf
    <number[]>;
    type Num = number
  • Key passed: Use the Extract approach for object properties

    type Order =
    type ElementOf<T, K extends keyof T = never> = [K] extends [never] ? T extends readonly unknown[] ? T[number] : never : Extract<T[K], readonly unknown[]>[number]
    ElementOf
    <
    interface Store
    Store
    , "orders">;
    type Order = {
    id: string;
    total: number;
    }

The tuple syntax [K] extends [never] is important since it prevents TypeScript from distributing the check over union types, which would break the logic.

The Extract trick: When a key is passed, Extract<T[K], readonly unknown[]>[number] does three things:

  1. Access the property: T[K]{ id: string; total: number }[] | undefined
  2. Extract only the array type: Extract<..., readonly unknown[]> isolates the array type from the union, discarding undefined{ id: string; total: number }[]
  3. Get the element type: [number]{ id: string; total: number }

By extracting only the array type before indexing, we avoid the type error.

In other words, this single helper covers both raw arrays and object properties without needing two separate utilities.

Edge case note: For non-array properties, this helper returns never:

interface
interface Store
Store
{
Store.name: string
name
: string;
}
Message: never as intended.
type Invalid =
type ElementOf<T, K extends keyof T = never> = [K] extends [never] ? T extends readonly unknown[] ? T[number] : never : Extract<T[K], readonly unknown[]>[number]
ElementOf
<
interface Store
Store
, "name">;
type Invalid = never

Conclusion

We started with T[number] and ended with a strict-safe helper. Along the way we learned:

  • Use T['prop'][number] if the property is always an array.
  • Use ElementOf<T> for raw arrays and tuples.
  • Use ElementOf<T, 'prop'> for array properties, especially if they’re optional.

Next time you need to extract an array element type, you’ll know exactly which tool fits, from a quick one-liner to a strict-safe helper.

Questions or Feedback?

I'd love to hear your thoughts, questions, or feedback about this post.

Reach me via email or LinkedIn.

Last updated on

Back to all posts