To understand the keyof typeof
usage in Typescript, first you need to understand what are literal types and union of literal types. So, I'll explain these concepts first and then explain keyof
and typeof
individually in detail. After that, I'll come back to enum
to answer what is asked in the question. It's a long answer but examples are easy to understand.
Literal types
Literal types in Typescript are more specific types of string
, number
or boolean
. For example, "Hello World"
is a string
, but a string
is not "Hello World"
. "Hello World"
is a more specific type of type string
, so it is a literal type.
A literal type can be declared as following:
type Greeting = "Hello"
This means that the object of type Greeting
can have only a string
value "Hello"
and no other string
value or any other value of any other type as shown in the following code:
let greeting: Greeting
greeting = "Hello" // OK
greeting = "Hi" // Error: Type '"Hi"' is not assignable to type '"Hello"'
Literal types are not useful on their own, however when combined with union types, type aliases and type guards they become powerful.
Following is an example of union of literal types:
type Greeting = "Hello" | "Hi" | "Welcome"
Now the object of type Greeting
can have the value either "Hello"
, "Hi"
or "Welcome"
.
let greeting: Greeting
greeting = "Hello" // OK
greeting = "Hi" // OK
greeting = "Welcome" // OK
greeting = "GoodEvening" // Error: Type '"GoodEvening"' is not assignable to type 'Greeting'
keyof
only
keyof
of some type T
gives you a new type that is a union of literal types and these literal types are the names of the properties of T
. The resulting type is a subtype of string.
For example, consider the following interface
:
interface Person {
name: string
age: number
location: string
}
Using the keyof
operator on the type Person
will give you a new type as shown in the following code:
type SomeNewType = keyof Person
This SomeNewType
is a union of literal types ("name" | "age" | "location"
) that is made from the properties of type Person
.
Now you can create objects of type SomeNewType
:
let newTypeObject: SomeNewType
newTypeObject = "name" // OK
newTypeObject = "age" // OK
newTypeObject = "location" // OK
newTypeObject = "anyOtherValue" // Error...
keyof typeof
together on an object
As you might already know, the typeof
operator gives you the type of an object.
In the above example of Person
interface, we already knew the type, so we just had to use the keyof
operator on type Person
.
But what to do when we don't know the type of an object or we just have a value and not a type of that value like the following?
const bmw = { name: "BMW", power: "1000hp" }
This is where we use keyof typeof
together.
The typeof bmw
gives you the type: { name: string, power: string }
And then keyof
operator gives you the literal type union as shown in the following code:
type CarLiteralType = keyof typeof bmw
let carPropertyLiteral: CarLiteralType
carPropertyLiteral = "name" // OK
carPropertyLiteral = "power" // OK
carPropertyLiteral = "anyOther" // Error...
keyof typeof
on an enum
In Typescript, enums are used as types at compile-time to achieve type-safety for the constants but they are treated as objects at runtime. This is because, they are converted to plain objects once the Typescript code is compiled to Javascript. So, the explanation of the objects above is applicable here too. The example given by OP in the question is:
enum ColorsEnum {
white = '#ffffff',
black = '#000000',
}
Here ColorsEnum
exists as an object at runtime, not as a type. So, we need to invoke keyof typeof
operators together as shown in the following code:
type Colors = keyof typeof ColorsEnum
let colorLiteral: Colors
colorLiteral = "white" // OK
colorLiteral = "black" // OK
colorLiteral = "red" // Error...
That's it! Hope that helps.