Question

Fixing Type 'string' Is Not Assignable to Type in TypeScript Union Types

typescriptjavascript

Question

In TypeScript, I defined a custom type in fruit.ts like this:

export type Fruit = "Orange" | "Apple" | "Banana";

In another file, I import that type and write code like this:

let myString: string = "Banana";
let myFruit: Fruit = myString;

When I assign myString to myFruit, TypeScript shows this error:

Type 'string' is not assignable to type '"Orange" | "Apple" | "Banana"'

How can I safely assign a string to a variable of the custom type Fruit?

Short Answer

By the end of this page, you will understand why a general string cannot be assigned to a narrower union type like Fruit, how TypeScript treats string literal types, and the safe ways to convert or validate a string before using it as a custom union type.

Concept

TypeScript distinguishes between broad types and narrow types.

  • string means any possible string value
  • "Banana" | "Apple" | "Orange" means only one of those exact strings

Your Fruit type is a string literal union type:

type Fruit = "Orange" | "Apple" | "Banana";

This is narrower than string.

So even if a variable currently contains "Banana", if its type is declared as string, TypeScript must assume it could later contain:

"Grape"
"Car"
"Hello"

Because of that, this is not safe:

let myString: string = "Banana";
let myFruit: Fruit = myString; // Error

TypeScript is protecting you from assigning a possibly invalid string to a variable that only allows a small fixed set of values.

This matters in real programming because union types are often used to model:

  • allowed API status values
  • user roles
  • configuration modes
  • event names
  • form field options

They help catch mistakes early and make code easier to reason about.

Mental Model

Think of string as a huge box of all labels and Fruit as a small basket that only accepts three labels:

  • string box: any label at all
  • Fruit basket: only Orange, Apple, or Banana

If someone hands you a value from the huge string box, you cannot just drop it into the Fruit basket unless you check that it is one of the allowed labels.

Another way to think about it:

  • string = "I know this is text"
  • Fruit = "I know this is one of these exact texts"

TypeScript needs that extra certainty before allowing the assignment.

Take Quiz

Syntax and Examples

The main ways to work with this are:

1. Declare the value as Fruit from the start

If the value is known to be valid, give it the narrower type immediately.

type Fruit = "Orange" | "Apple" | "Banana";

let myFruit: Fruit = "Banana";

This works because "Banana" is one of the allowed values.

2. Let TypeScript infer the literal type

If you use const, TypeScript usually keeps the exact literal value.

type Fruit = "Orange" | "Apple" | "Banana";

const myString = "Banana";
const myFruit: Fruit = myString;

Here, myString is inferred as "Banana", not string, so the assignment is valid.

3. Validate the string before assigning

If the value comes from user input, an API, or another dynamic source, check it first.

type Fruit = "Orange" | "Apple" | "Banana";

function isFruit(value: string): value is Fruit {
  return value === "Orange" || value === "Apple" || value === "Banana";
}

let myString: string = "Banana";
let myFruit: Fruit;

if (isFruit(myString)) {
  myFruit = myString;
}

This is the safest approach for runtime data.

4. Use a type assertion only when you are certain

type Fruit = "Orange" | "Apple" | "Banana";

let myString: string = "Banana";
let myFruit: Fruit = myString as Fruit;

This tells TypeScript to trust you. It removes the error, but it does not validate the value at runtime.

If myString is actually "Grape", your program can still behave incorrectly.

Use assertions carefully.

Step by Step Execution

Consider this example:

type Fruit = "Orange" | "Apple" | "Banana";

function isFruit(value: string): value is Fruit {
  return value === "Orange" || value === "Apple" || value === "Banana";
}

const input: string = "Banana";
let selectedFruit: Fruit | undefined;

if (isFruit(input)) {
  selectedFruit = input;
}

Step by step:

  1. Fruit is defined as a union of three exact strings.
  2. isFruit takes any string.
  3. Inside isFruit, the function checks whether the value matches one of the allowed fruit names.
  4. input is typed as string, so TypeScript does not assume it is a valid Fruit.
  5. selectedFruit can be either a valid Fruit or undefined.
  6. The if (isFruit(input)) check runs.
  7. Because isFruit is a type guard, TypeScript narrows input from string to Fruit inside the if block.
  8. Now selectedFruit = input is allowed.

If input had been "Grape", the condition would fail and the assignment would not happen.

That is both type-safe and runtime-safe.

Real World Use Cases

String literal union types are common in real applications.

API response statuses

type Status = "idle" | "loading" | "success" | "error";

This prevents invalid states like "done" when your app only supports four known statuses.

User roles

type Role = "admin" | "editor" | "viewer";

You can validate input from a database or request before assigning it to Role.

Configuration values

type Theme = "light" | "dark";

This ensures only supported theme names are used.

Form options

type PaymentMethod = "card" | "cash" | "bank-transfer";

This helps prevent bugs caused by misspelled values.

Event names

type EventName = "click" | "submit" | "close";

This gives autocomplete and catches invalid event strings during development.

Take Quiz

Real Codebase Usage

In real codebases, developers usually avoid assigning plain string values directly to narrow union types unless the value is already trusted.

Common patterns

Guard clauses for validation

function parseFruit(value: string): Fruit | undefined {
  if (value !== "Orange" && value !== "Apple" && value !== "Banana") {
    return undefined;
  }

  return value;
}

This pattern is common when processing request data, query parameters, or form input.

Early return for invalid input

function handleFruit(value: string) {
  if (value !== "Orange" && value !== "Apple" && value !== "Banana") {
    throw new Error("Invalid fruit");
  }

  const fruit: Fruit = value;
  console.log(fruit);
}

Constant lists plus derived types

A common pattern is to keep allowed values in one place.

const FRUITS = ["Orange", "Apple", "Banana"] as const;
type Fruit = typeof FRUITS[number];

function isFruit(value: string): value is Fruit {
  return FRUITS.includes(value as Fruit);
}

This reduces duplication.

Validation at boundaries

Most real apps validate external data at boundaries:

  • HTTP requests
  • environment variables
  • local storage
  • database records
  • JSON files

After validation, the rest of the code can safely use the narrower type.

Common Mistakes

Mistake 1: Declaring a literal as string

let myString: string = "Banana";
let myFruit: Fruit = myString; // Error

Why it happens:

  • You told TypeScript this variable can hold any string.

How to avoid it:

  • Use const when the value does not change.
  • Use Fruit directly if it should always be a valid fruit.
const myString = "Banana";
const myFruit: Fruit = myString;

Mistake 2: Using as Fruit without validation

const input: string = "Grape";
const fruit = input as Fruit;

Why it is dangerous:

  • TypeScript trusts you.
  • Runtime data is still invalid.

How to avoid it:

  • Prefer a type guard.

Mistake 3: Assuming TypeScript checks values at runtime

TypeScript types disappear after compilation.

type Fruit = "Orange" | "Apple" | "Banana";

This helps during development, but it does not automatically reject bad user input at runtime.

How to avoid it:

  • Add real runtime checks for external values.

Mistake 4: Forgetting const preserves literal values

let fruitName = "Banana"; // usually inferred as string
const fruitName2 = "Banana"; // inferred as "Banana"

Use const when possible if you want narrower inference.

Mistake 5: Duplicating allowed values in many places

type Fruit = "Orange" | "Apple" | "Banana";

function isFruit(value: string): boolean {
  return value === "Orange" || value === "Apple" || value === "Banana";
}

This works, but duplication becomes hard to maintain.

Better approach:

const FRUITS = ["Orange", "Apple", "Banana"] as const;
type Fruit = typeof FRUITS[number];

Comparisons

ConceptMeaningExampleSafe for dynamic input?
stringAny text valuelet x: string = "Banana"No, too broad
String literalOne exact text valueconst x = "Banana"Only if the value is known
Union of literalsOne of a fixed set`type Fruit = "Apple""Banana"`
Type assertionForce TypeScript to trust youx as FruitRisky
Type guardCheck value before narrowingisFruit(x)Yes

string vs Fruit

let a: string = "Banana";
let b: Fruit = "Banana";
  • a can later become any string.
  • b can only ever be one of the allowed fruit values.

Type assertion vs type guard

Type assertion

const fruit = input as Fruit;
  • Short
  • No runtime validation
  • Can hide bugs

Type guard

if (isFruit(input)) {
  const fruit: Fruit = input;
}
  • Slightly more code
  • Runtime-safe
  • Preferred for external data
Take Quiz

Cheat Sheet

type Fruit = "Orange" | "Apple" | "Banana";

Rules

  • string is broader than Fruit
  • You cannot assign string to Fruit without narrowing
  • const x = "Banana" is often inferred as the literal "Banana"
  • let x = "Banana" is usually inferred more broadly
  • as Fruit bypasses checking and should be used carefully

Safe patterns

Known valid value

const fruit: Fruit = "Banana";

Narrow via inference

const value = "Banana";
const fruit: Fruit = value;

Narrow via validation

function isFruit(value: string): value is Fruit {
  return value === "Orange" || value === "Apple" || value === "Banana";
}

Parse helper

function parseFruit(value: string): Fruit | undefined {
  return isFruit(value) ? value : undefined;
}

Avoid

let value: string = "Banana";
let fruit: Fruit = value; // Error

Safer constant-array pattern

const FRUITS = ["Orange", "Apple", "Banana"] as const;
type Fruit = typeof FRUITS[number];

FAQ

Why does TypeScript reject a string that contains "Banana"?

Because the variable is typed as string, which means it could contain any string, not just allowed fruit values.

Can I force the assignment with as Fruit?

Yes, but it only silences TypeScript. It does not validate the actual value at runtime.

What is the safest way to convert a string to Fruit?

Use a type guard or validation function that checks the string before assigning it.

Why does const myString = "Banana" work better than let myString: string = "Banana"?

const usually preserves the exact literal type, while string is a broader type.

When should I use a union type like Fruit?

Use it when only a fixed set of string values is valid, such as statuses, roles, modes, or options.

Does TypeScript validate union types at runtime?

No. TypeScript only checks types during development and compilation. Runtime validation must be written explicitly.

Take Quiz

Mini Project

Description

Build a small TypeScript utility that accepts a string from an external source and safely turns it into a valid Fruit value. This demonstrates how union types and type guards work together in real code where input cannot be trusted automatically.

Goal

Create a function that validates a string and returns a safe Fruit result or an error message.

Requirements

  1. Define a Fruit union type with "Orange", "Apple", and "Banana".
  2. Create a type guard function to check whether a string is a valid fruit.
  3. Write a parser function that accepts a string and returns either a Fruit or undefined.
  4. Test the parser with both valid and invalid input values.
  5. Print clear output showing which values were accepted and which were rejected.
Take Quiz

Keep learning

Related questions

View more