Objects and unions
TypeScript type for a string or array of strings
Learn how to declare a TypeScript type that can be either a string or an array of strings using a union, written as string or string[].
Simple one, but I forgot the exact syntax and had to search for it, so let me write it down:
export interface Props {
tag: string | string[]
}
tag can be a string, or an array of strings.
The | creates a union type. The property accepts either form, and nothing else. The same works with a type alias:
type Props = {
tag: string | string[]
}
I hit this writing an Astro component that takes either one tag or a list of tags. I wanted both call sites to work, and with the union they do:
const one: Props = { tag: 'typescript' }
const many: Props = { tag: ['typescript', 'javascript'] }
Try to sneak a number into the array and TypeScript refuses:
const bad: Props = { tag: ['typescript', 42] }
error TS2322: Type 'number' is not assignable to type 'string'.
Reading the value back
The union is convenient for callers. It puts a small burden on the reading side. When you read tag, TypeScript only allows operations that exist on both members of the union. Call an array method directly and it complains:
error TS2339: Property 'map' does not exist on type 'string | string[]'.
Property 'map' does not exist on type 'string'.
The second line is the useful one. map() exists on string[], but tag might be a plain string, and strings have no map().
The fix is to narrow with Array.isArray(). That is the runtime check TypeScript understands for arrays:
function normalizeTags(tag: string | string[]) {
return Array.isArray(tag) ? tag : [tag]
}
When the check is true, tag is a string[] and comes back as-is. When it is false, tag must be a string, so wrapping it in brackets produces an array. The return type is inferred as string[] without any annotation.
This is the pattern I recommend. Accept two convenient input shapes at the boundary, then normalize them right away. Call normalizeTags() once at the top of the component, and the rest of the code works with a plain string[]. No Array.isArray() checks scattered everywhere.
The same union works anywhere, not just in component props. A function parameter typed string | string[] gives callers the same flexibility, with the same one-line normalization inside.
Try this on your own: hover over the result of normalizeTags('typescript') and confirm it is string[]. Then change the return to Array.isArray(tag) ? tag : tag and read the new inferred type.
Lesson completed