你不知道的 TypeScript
对象变量初始化的类型推断
现有如下代码:
const rep = {url: "https://www.typescriptlang.org", method: "GET"};
function respond(url: string, method: "GET" | "POST"){
...
}
那么请你判断一下像下面这样调用 respond 函数是否正确。
respond(rep.url, rep.method);
结果第二个参数会被 ts 标红,你答对了吗?因为当你在对象内部初始化一个变量时,ts 推测将来这个值是有可能改变的,所以我们在 rep 对象中声明的 method为 "GET" ,在实际调用 respond(rep.url, rep.method)时,第二个参数推测出来的类型为 string, 与 respond 方法要求的第二个参数的联合类型不符,所以会被 ts 标红。
那么如何解决呢?
- 方法一:声明对象后使用类型断言将其断言为 const ,这样在对象内初始化的值就会被 ts 推断为对应的字面量类型。
const rep = {url: "https://www.typescriptlang.org", method: "GET"} as const;
- 方法二: 变量初始化时使用类型断言。
const rep = {url: "https://www.typescriptlang.org", method: "GET" as "GET"};
- 方法三: 调用方法时使用类型断言。
respond(rep.url, rep.method as "GET");
数字枚举本身就是联合类型?
ts 的数字枚举本身就是一个包含其所有变量值的有效联合类型。
eg:
enum enumType {
a,
b,
c
}
function test(param: enumType){
...
}
test(param); param 必须为 0 | 1 | 2;
注意:
- 枚举类型创建时必须初始化,上述枚举类型没有初始化会默认从第一个变量依次向后递增初始化: 0 1 2 ...
- 只有数字枚举作为类型使用时是其所有变量值的有效联合类型。
当你想要一个枚举类型的所有 key 组合为一个联合类型该怎么办?
enum LogLevel {
ERROR,
WARN,
INFO,
DEBUG,
}
// 下列代码等价于:type LogLevelStrings = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
type LogLevelStrings = keyof typeof LogLevel;
当你想要一个枚举类型的所有 key 的 value 组合为一个联合类型该怎么办?
enum LogLevel {
ERROR,
WARN,
INFO,
DEBUG,
}
// 下列代码等价于:type LogLevelStrings = 0 | 1 | 2| 3;
type LogLevelStrings = typeof LogLevel[keyof typeof LogLevel];
numeric enum 的反向索引
当我们创建如下枚举,并将其在控制台打印可得到:
enum numeric {
a,
b,
c
}
console.log(numeric);
// 打印结果
{
"0": "a",
"1": "b",
"2": "c",
"a": 0,
"b": 1,
"c": 2
}
也就是说,对于数字枚举,我们不光可以通过 key 拿到 value,还可以通过 value 拿到对应的 key。
注意:仅数字枚举可以。
const 枚举
const 枚举比普通枚举更加严格,const 枚举在编译后是会被完全删除的,因此 const 枚举只支持常量枚举表达式,所以不能有计算成员。 同时,const 枚举只能在属性、索引访问表达式(即 obj.key 或 obj[key])、导入声明、导出赋值、作为类型使用。
const enum numeric {
a="1",
b="c",
c=genNumber() // 这里会被 ts 标红,const 枚举不能有计算成员
}
function genNumber(){
return 1
}
const 和 typeof 的妙用
通过 const 和 typeof 可以从一个对象身上衍生出与之有关联的类型。
const obj1 = {
name: 'iceberg',
age: 23
};
// 等价于 type person = {name: string, age: number}
type person = typeof obj1;
const obj1 = {
name: 'iceberg',
age: 23
} as const;
// 等价于 type person = {name: 'iceberg', age: 23}
type person = typeof obj1;
那如果想获取对象所有值的联合类型怎么做呢?可以这样实现:
// 已有对象 ODirection 来表示某一函数的参数结构
const ODirection = {
Up: 0,
Down: 1,
Left: 2,
Right: 3,
}
// 将对象整体通过类型断言为常量
const ODirection = {
Up: 0,
Down: 1,
Left: 2,
Right: 3,
} as const;
type Direction = typeof ODirection[keyof typeof ODirection];
// 此时 type Direction = 0 | 1 | 2 | 3;
// 后续使用可以十分方便
//eg:
function drive(direction: Direction){
...
}
drive(ODirection.Up)
为什么 ts 要支持这种语法? 我理解的应用场景是原有项目中已经存在描述参数类型的对象,需要使其适配ts时,该语法可以发挥极大的作用,它可以在最大化的保留原始对象结构的情况下,可以更好的体验ts类型检查:
- 直接声明
type Direction = 0 | 1 | 2 | 3;第一个问题就是丢失了和对象 ODirection 的关联性,后续 ODirection 对象变化后,需要手动修改 Direction。 - 将对象 ODirection 断言为 const 后,再通过
type Direction = typeof ODirection[keyof typeof ODirection];的方式提取出联合类型,最大化的利用了原始 ODirection 对象,同时保留了两者的关联性。后续往 ODirection 对象中添加了新的属性时,联合类型中会相应的自动扩展。
interface(接口)和 type(类型别名)的区别
关键区别:类型不能重新开放以添加新的属性,但接口始终是可以扩展的。 换句话说就是:类型不能重新声明来添加新的属性(不支持声明合并),但接口可以重复声明来实现扩展。
interface Person {
name: string;
}
// 接口可以重复声明从而扩展
interface Person {
age: number;
}
type Animal = {
name: string;
}
// 类型重复声明会报错
type Animal = {
age: number;
}
如何有效扩展接口和类型别名呢?
- 扩展接口
interface Animal {
name: string
}
interface Bear extends Animal {
honey: boolean
}
const bear = getBear()
bear.name
bear.honey
- 扩展类型
type Animal = {
name: string
}
type Bear = Animal & {
honey: Boolean
}
const bear = getBear();
bear.name;
bear.honey;
接口和类型别名的功能差不多,那什么时候采用接口,什么时候采用类型别名?
场景一: 当我们项目中编程思想倾向于面向对象时,应该优先考虑使用接口,因为接口更加贴向面向对象的编程思想,而且提供了可选属性、只读属性、声明合并等高级特性。 场景二:当我们需要简化复杂类型定义或者类型定义需要在多个地方被复用时,应该优先考虑类型别名,因为它可以提供一个简洁的引用,同时简化代码可读性。
复杂场景下如何利用 TypeScipt 进行优雅封装?
场景如下:假设你正在开发一个复杂的 UI 库组件,该库包含多种类型的按钮组件(普通按钮、加载按钮、危险按钮等)。没种组件都有其特定的属性和行为,也有其公共的属性和行为,如何通过 ts 优雅的对其进行封装?
interface ButtonProps {
label: string;
onClick?: () => void;
}
interface ButtonInstance {
getProps(): ButtonProps;
render(): void;
// 其他方法
}
// 定义一个 Button 构造函数类型
type ButtonConstructor = {
new(props: ButtonProps): ButtonInstance;
}
// Normal Button
class NormalButton implements ButtonInstance {
private props: ButtonProps;
constructor(props: ButtonProps) {
this.props = props;
}
render() {
console.log('render normal button',this.props);
}
getProps() {
return this.props;
}
}
// Loading Button
class LoadingButton implements ButtonInstance {
private props: ButtonProps;
constructor(props: ButtonProps) {
this.props = props;
}
render() {
console.log('render loading button',this.props);
}
getProps() {
return this.props;
}
}
// Danger Button
class DangerButton implements ButtonInstance {
private props: ButtonProps;
constructor(props: ButtonProps) {
this.props = props;
}
render() {
console.log('render danger button',this.props);
}
getProps() {
return this.props;
}
}
function createButton(ctor: ButtonConstructor, props: ButtonProps): ButtonInstance {
return new ctor(props);
}
const normalButton = createButton(NormalButton, { label: 'normal button', onClick: () => console.log('click normal button')});
const loadingButton = createButton(LoadingButton, { label: 'loading button', onClick: () => console.log('click loading button') });
const dangerButton = createButton(DangerButton, { label: 'danger button', onClick: () => console.log('click danger button') });
normalButton.render();
loadingButton.render();
dangerButton.render();
上面简单的一个示例已经写完,看完后是否觉得以上行为纯属脱了裤子放屁,我直接写一个 NormalButton 类,然后使用时直接 new 它不好吗?搞这么复杂干什么?简单场景下来说确实如此,但是场景一旦复杂起来,处理起来就要棘手很多,我先分析一下上述代码,看完分析后你应该会有所收获:
- 我们首先创建了一个接口
ButtonProps用于定义按钮组件的属性。 - 然后我们创建了一个接口
ButtonInstance用于定义按钮组件实例的行为。 - 接着我们通过类型别名定义了一个
ButtonConstructor构造函数类型,内部定义了构造函数的参数props,其类型为ButtonProps,最后该构造函数的返回值类型为ButtonInstance实例。 - 然后我们分别按照
ButtonInstance接口实现了三个不同按钮的类。 - 最后我们实现一个专门用于创建
Button实例的工厂函数。
通过上述面向对象编程思想封装出的组件,有着非常多的好处:
- 借助于 TS 的类型系统提供了强大的类型检查和推断功能,能够有效确保代码的鲁棒性。
- 定义共享的类型别名和接口,多处复用代码,减少重复。
- 灵活性极高,将按钮实例的创建逻辑封装在工厂函数中,可根据场景需要动态创建不同的按钮实例。
- 可扩展性极高,后续要扩展什么功能直接在各自的类里面实现即可。
内容如有不对,欢迎指正~