Question
Fixing Type 'string' Is Not Assignable to Type in TypeScript Union Types
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.
stringmeans 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:
stringbox: any label at allFruitbasket: onlyOrange,Apple, orBanana
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.
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:
Fruitis defined as a union of three exact strings.isFruittakes anystring.- Inside
isFruit, the function checks whether the value matches one of the allowed fruit names. inputis typed asstring, so TypeScript does not assume it is a validFruit.selectedFruitcan be either a validFruitorundefined.- The
if (isFruit(input))check runs. - Because
isFruitis a type guard, TypeScript narrowsinputfromstringtoFruitinside theifblock. - Now
selectedFruit = inputis 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.
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
constwhen the value does not change. - Use
Fruitdirectly 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
| Concept | Meaning | Example | Safe for dynamic input? |
|---|---|---|---|
string | Any text value | let x: string = "Banana" | No, too broad |
| String literal | One exact text value | const x = "Banana" | Only if the value is known |
| Union of literals | One of a fixed set | `type Fruit = "Apple" | "Banana"` |
| Type assertion | Force TypeScript to trust you | x as Fruit | Risky |
| Type guard | Check value before narrowing | isFruit(x) | Yes |
string vs Fruit
let a: string = "Banana";
let b: Fruit = "Banana";
acan later become any string.bcan 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
Cheat Sheet
type Fruit = "Orange" | "Apple" | "Banana";
Rules
stringis broader thanFruit- You cannot assign
stringtoFruitwithout narrowing const x = "Banana"is often inferred as the literal"Banana"let x = "Banana"is usually inferred more broadlyas Fruitbypasses 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.
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
- Define a
Fruitunion type with"Orange","Apple", and"Banana". - Create a type guard function to check whether a string is a valid fruit.
- Write a parser function that accepts a
stringand returns either aFruitorundefined. - Test the parser with both valid and invalid input values.
- Print clear output showing which values were accepted and which were rejected.
Keep learning
Related questions
@Directive vs @Component in Angular: Differences, Use Cases, and When to Use Each
Learn the difference between @Directive and @Component in Angular, including use cases, examples, and when to choose each.
Accessing Input Value from EventTarget in TypeScript
Learn why EventTarget has no value property in TypeScript and safely read values from HTML input events in Angular applications.
Angular (change) vs (ngModelChange): What’s the Difference?
Learn the difference between Angular (change) and (ngModelChange), when each fires, and which one to use in forms and inputs.