I'm trying to map a property of type number to a property of type string literals (by union).
Simplified example:
interface Destination {
stringLiteralNumber: '0' | '1' | '2';
}
// Assume pre-validated to be between 0 - 2
const actualNumber: number = 1;
const mapped: Destination = {
// How to map so Typescript is happy?
stringLiteralNumber: actualNumber,
}
It's not that easy in the case where the actualNumber
type is number
. If you had the type narrowed to the literal union of each allowed number (i.e. 0 | 1 | 2
), then you could use string interpolation (e.g. ${actualNumber}
) to get the correct type. This is different than casting it using the String
constructor, like shown below...
type MyNumber = 0 | 1 | 2;
type MyNumberString = '0' | '1' | '2';
interface Destination {
stringLiteralNumber: MyNumberString;
}
const actualNumber: MyNumber = 1; // typed as 0 | 1 | 2
const mapped: Destination = {
stringLiteralNumber: String(actualNumber),
//^^^^^^^^^^^^^^^^^^^ Error: Type 'string' is not assignable to type 'MyNumberString'.
}
So if you haven't narrowed the type, you could use a type predicate to manually narrow the type. Although, this would require duplicating the validation, then using a type guard.
type MyNumber = 0 | 1 | 2;
type MyNumberString = '0' | '1' | '2';
interface Destination {
stringLiteralNumber: MyNumberString;
}
const actualNumber: MyNumber = 1;
function isMyNumber(n: number): n is MyNumber {
return [0, 1, 2].includes(n); // return boolean to manually validate type
}
if (isMyNumber(actualNumber)) {
// inside this block `actualNumber` is `MyNumber` type
const mapped: Destination = {
stringLiteralNumber: `${actualNumber}`,
}
}
Type predicates can be dangerous because they can be wrong.
function everythingIsString(value: unknown): value is string {
return true; // always true no matter the input
}
But this approach is better than using as
.
Really you should try to set this value to the correct type where you are doing the validation, then pass it wherever you need. That could look something like this...
type MyNumber = 0 | 1 | 2;
type MyNumberString = '0' | '1' | '2';
interface Destination {
stringLiteralNumber: MyNumberString;
}
function isMyNumber(n: number): n is MyNumber {
return [0, 1, 2].includes(n);
}
const someNumber: number = 2
// `someNumber` is of type `number`
if (!isMyNumber(someNumber)) {
throw new Error('Bad validation');
}
// `someNumber` is of type `MyNumber`
const someNumberString: MyNumberString = `${someNumber}` // No error 👍
PS - Im assuming you posted your question correctly as
'0' | '1' | '2'
, but it it was meant to be0 | 1 | 2
that would change things