4. TypeScript基础(下):函数定义、箭头函数、联合类型、泛型入门、模块导入导出

好,咱们接着往下聊。上一节我们把TypeScript的基础类型和接口过了一遍,这一节要讲的东西,说白了就是你在写ArkUI代码时每天都会碰到的——函数怎么定义、类型怎么灵活组合、还有怎么把代码拆成模块。

我个人觉得,这一节的内容比上一节更贴近实战。你想想看,一个鸿蒙应用少说几十个文件,函数成百上千,如果类型写不好,后面维护起来真的很痛苦。我曾经在一个项目里见过一个函数,参数类型写了any,结果调用的时候传了个字符串进去,运行时直接崩了……嗯,从那以后我对类型定义就特别较真。

4.1 函数定义:从基础到重载

TypeScript里的函数,和JavaScript最大的区别就是——参数和返回值都得有类型。别嫌麻烦,这其实是帮你省事。

// 基础函数定义
function greet(name: string): string {
    return `你好,${name}!`;
}

// 可选参数
function buildUrl(base: string, path?: string): string {
    return path ? `${base}/${path}` : base;
}

// 默认参数
function createButton(text: string, width: number = 100): string {
    return `按钮: ${text}, 宽度: ${width}`;
}

// 剩余参数
function sum(...numbers: number[]): number {
    return numbers.reduce((a, b) => a + b, 0);
}
我的习惯:可选参数尽量放在参数列表的最后面。这不是我瞎说的,TypeScript编译器会检查这个顺序,放前面会报错。

函数重载这个特性,说实话在ArkUI开发中用得不算特别频繁,但一旦遇到就很香。比如你要写一个处理事件的方法,参数可能是点击事件也可能是滑动事件,这时候重载就派上用场了。

// 函数重载
function handleEvent(event: ClickEvent): void;
function handleEvent(event: SwipeEvent): void;
function handleEvent(event: any): void {
    if (event.type === 'click') {
        console.log('处理点击');
    } else {
        console.log('处理滑动');
    }
}
注意:重载的签名只是声明,真正的实现只有一个。我曾经见过有人写了三个重载签名,结果实现签名参数类型写错了,编译能过但运行时行为诡异。所以实现签名一定要用最宽泛的类型。

4.2 箭头函数:简洁与this的陷阱

箭头函数在ArkUI里太常见了,尤其是写事件回调的时候。它的好处有两个:一是语法简洁,二是不绑定自己的this。

// 普通函数 vs 箭头函数
const numbers = [1, 2, 3];

// 普通函数
const doubled1 = numbers.map(function(n) {
    return n * 2;
});

// 箭头函数
const doubled2 = numbers.map(n => n * 2);

为什么会说this是个陷阱?我刚开始写TypeScript时也踩过这个坑。在ArkUI的组件方法里,如果你用普通函数定义回调,this指向可能会丢失。箭头函数就没有这个问题,因为它会捕获定义时的this。

class MyComponent {
    private count: number = 0;

    // 这样写没问题
    onClick = () => {
        this.count++;
    }

    // 这样写可能会出问题
    onSwipe() {
        // 这里的this可能不是MyComponent的实例
        this.count++; // 运行时可能报错
    }
}
避坑指南:在ArkUI的@State装饰的变量回调里,我建议一律用箭头函数。我曾经因为用了普通函数,导致状态更新后界面不刷新,排查了半天才发现是this指向问题。

4.3 联合类型:让类型更灵活

联合类型说白了就是「或」的关系。一个变量可以是类型A,也可以是类型B。这在处理多种可能的数据时特别有用。

// 基础联合类型
let id: string | number;
id = 'abc123';  // 正确
id = 456;       // 正确
// id = true;   // 错误

// 联合类型与函数
function getLength(value: string | string[]): number {
    if (typeof value === 'string') {
        return value.length;
    }
    return value.length;
}

这里有个关键点——类型收窄。当你用typeof或者instanceof判断之后,TypeScript会自动缩小类型的范围。我个人习惯在写联合类型参数时,第一时间做类型判断,这样后面的代码就安全了。

// 类型收窄
function formatInput(input: string | number): string {
    // 先收窄类型
    if (typeof input === 'string') {
        return input.toUpperCase();
    }
    // 到这里TypeScript知道input一定是number
    return input.toFixed(2);
}

4.4 泛型入门:写一次,用多种类型

泛型这个概念,很多初学者会觉得抽象。我换个说法——你写一个函数,不想把参数类型写死,但又不想用any丢掉类型检查,这时候泛型就来了。

// 基础泛型函数
function firstElement<T>(arr: T[]): T | undefined {
    return arr[0];
}

// 使用
const num = firstElement([1, 2, 3]);  // num类型是 number
const str = firstElement(['a', 'b']); // str类型是 string

在ArkUI开发中,泛型最常用的场景是封装列表组件。比如你要写一个通用的列表,不管数据是用户信息还是商品信息,都能复用。

// 泛型接口
interface ListProps<T> {
    data: T[];
    renderItem: (item: T, index: number) => void;
}

// 泛型约束
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

const user = { name: '张三', age: 25 };
const userName = getProperty(user, 'name'); // 类型是 string
我的建议:泛型约束里的extends keyof是个很实用的技巧。它保证了你只能访问对象确实存在的属性,不会出现拼写错误。我在封装数据请求层时经常用这个。

4.5 模块导入导出:组织代码的艺术

模块化是大型项目的基石。TypeScript沿用了ES Module的语法,说白了就是export和import。

// 文件: types.ts
export interface User {
    id: number;
    name: string;
}

export const DEFAULT_PAGE_SIZE = 20;

// 默认导出
export default function createUser(name: string): User {
    return { id: Date.now(), name };
}
// 文件: app.ts
import createUser, { User, DEFAULT_PAGE_SIZE } from './types';

const newUser = createUser('李四');
console.log(newUser.name); // 李四

这里有个细节——默认导出和命名导出的区别。我个人习惯一个文件只做一个默认导出,其他辅助类型和常量用命名导出。这样别人看你的代码时,一眼就知道这个文件主要提供什么功能。

// 重命名导入
import { User as AppUser } from './types';

// 整体导入
import * as Types from './types';
const user: Types.User = { id: 1, name: '王五' };
注意:在ArkUI的ets文件里,模块路径要写对。我曾经因为路径大小写写错了,在Windows上能编译通过,但到了鸿蒙设备上就报模块找不到。嗯,从那以后我都是复制路径,绝不手打。

4.6 实战小技巧:组合使用

学完这些知识点,咱们看看怎么组合起来用。下面是一个ArkUI里常见的场景——封装一个通用的数据请求Hook。

// api.ts
export interface ApiResponse<T> {
    code: number;
    data: T;
    message: string;
}

export async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
    const response = await fetch(url);
    return response.json();
}

// 使用
import { fetchData } from './api';

interface Product {
    id: number;
    name: string;
    price: number;
}

async function loadProducts() {
    const result = await fetchData<Product[]>('/api/products');
    if (result.code === 200) {
        console.log(result.data.length); // 类型安全
    }
}

你看,泛型保证了返回数据的类型,联合类型可以处理不同的响应状态,模块化让代码结构清晰。这些知识点串起来,就是你在实际项目中每天都会用到的技能。

好了,这一节的内容就到这里。下一节我们会讲枚举和类型别名,这两个在定义常量状态时特别有用。记得多动手写代码,光看是学不会的。