今天在看别人写的代码的时候,有个keyof和typeof的代码看不懂,下面就通过查找原理之后做个记录。
export const themeAlgorithm = {
light: defaultAlgorithm,
dark: darkAlgorithm,
} as const
export type ThemeType = keyof typeof themeAlgorithm
其实最后一行可以换种写法:
export type AppThemeType = 'light' | 'dark'
想要理解 TypeScript 里 keyof typeof
是如何工作的,首先需要理解什么是 字面量类型(literal types) 和 联合类型(union types).
字面量类型(literal types)
Typescript 中的字面量类型是更具体的string, number或boolean类型。比如“Hello World”是一个string, 但是string类型不是“Hello World”, 所以“Hello World”是一个字面量类型。
一个字面量类型可以被这样定义:
type A = "hello"
这表示A类型的对象只能有一个字符串“hello“,并且没有其他string类型的值或其他任何类型的值,比如如下代码:
let greeting: A
greeting = "hello" // OK
greeting = "hi" // Error: Type "hi" is not assignable to type "hello"
字面量类型本身并不是很有用,但是当它和联合类型(union types),类型别名(type aliases), 类型保护(type guards)组合起来后,就很有用了。
联合类型
如果希望属性为多种类型之一,如字符串或数组,这时联合类型就派上用场了(它使用 |(竖线) 作为标记,如string|number)。联合类型可以理解为多个类型的并集。联合类型表示变量、参数的类型不是某个单一的类型,而可能是多种不同类型的组合。
下面是联合字面量类型的例子:
type A = "Hello" | "Hi" | "Welcome"
现在A类型对象的值可以是“Hello”,“Hi”或“Welcome”
let greeting: A
greeting = "Hello" // OK
greeting = "Hi" // OK
greeting = "Welcome" // OK
greeting = "Other" // Error: Type '"Other"' is not assignable to type 'A'
类型缩减
如果定义的联合类型包含字符串字面量类型和string类型,会有什么效果呢?由于string类型是字符串字面量类型的父类型,所以最后会缩减为string类型。number和boolean在这种情况下也会发生类型缩减。
type UnionNum = 1 | number // 类型为number
type UnionStr = "str" | string // 类型为string
type UnionBoolean = false | boolean // 类型为boolean
在这种情况下,TypeScript会对类型进行缩减,将字面量类型去掉,保留原始类型。
但是这样也造成另一个问题:编译器只能提示我们定义的变量是那个原始的类型:
TypeScript提供了一种方式来控制类型缩减,只需给父类型添加”& {}" 即可:
此时,其他字面量类型就不会被缩减,在编辑器中字符串字面量str1,str等就可以自动提示出来了。
另外,当联合类型的成员是接口类型,并满足其中一个接口的属性是另一个接口属性的子集,这个属性也会进行类型缩减:
type UnionInterface = {age: "18"} | {age: "18" | "25", [key: string]: string}
由于“18”是“18”|“25”的子集,所以age属性的类型会变成“18”|“25”
类型别名(Type Alias)
联合类型如果很多,每次都需要写很长的类型定义,如下面的例子:
function f1(command: string[] | string){
let line = '';
if(typeof command === 'string'){
line = command.trim()
} else {
line = command.join(' ').trim()
}
}
可以看到这个方法的参数类型是一个联合类型,如果下次我们有个方法f2的参数也是一样的联合类型,我们就可以把这个参数的类型抽离出来,这个就叫做类型别名:
type Command = string[] | string
// 再次定义方法是可以这样写了
function f1(command : Command) {
// ....
}
function f2(param: Command) {
// ....
}
keyof单独使用
假设现在有个类型T, keyof T 将会给你一个新类型,它是我们前面提到的联合字面量类型,并且组成它的字面量类型是T的属性名称。最后生成的类型是字符串的子类型。
interface Person {
name: string
age: number
location: string
}
在Person类型上使用keyof, 将会得到一个新类型,如下面代码所示:
type newType = keyof Person
newType 就是一个联合字面量类型(“name” | “age”| “location”),它是由Person的属性组成的类型。
现在就可以创建newType类型的对象了:
keyof typeof 同时使用
typeof运算符为你提供对象的类型,上面例子中的Person interface,我们已经知道它的类型,所以只需要在Person上使用keyof操作符。
但是,当我们不知道对象的类型,或者我们只有一个值,类似开头的场景:
const themeAlgorithm = {
light: defaultAlgorithm,
dark: darkAlgorithm
}
这就需要我们一起使用keyof typeof的时候了。
typeof themeAlgorithm 给到你的类型是{light: string, dark: string}
接着 keyof 操作符给到你的是联合字面量类型 "light" | "dark",就像下面的代码描述的一样:
type ThemeType = keyof typeof themeAlgorithm
let applicationTheme: ThemeType
applicationTheme = "dark" // OK
applicationTheme = "light" // OK
applicationTheme = "otherTheme" // Error Type '"otherTheme"' is not assignable to type '"light" | "dark"'
在enum上使用keyof typeof
在TypeScript中,enum在编译时被用作类型,用来实现常量的类型安全,但是他们在运行时被视为对象。这时因为当TypeScript代码被编译为Javascript时,他们会被转换为普通对象。比如:
enum ColorsEnum {
white = '#ffffff',
black = '#000000'
}
ColorsEnum在运行时作为一个对象存在,不是一个类型,所以我们需要一起使用keyof typeof这两个操作符,像下面代码展示的一样。
type Color = keyof typeof ColorsEnum
let color:Color
color = "white" // OK
color = "black" // OK
color = "red" // ERROR Type '"red"' is not assignable to type '"white" | "black"'.