特殊

TypeScript 使用

类型安全的开发实践和常见问题

概述

本指南解释在 TypeScript 项目中使用 Æternity SDK 时可能遇到的边缘情况及处理方法。SDK 完全使用 TypeScript 编写,提供完整的类型定义。

提取 SDK 方法的类型

SDK 不单独导出类型以减少导出数量。使用 TypeScript 内置泛型 ParametersReturnType 提取类型:

提取参数类型
import { walletDetector } from '@aeternity/aepp-sdk';

// 提取 walletDetector 第二个参数(回调函数)的类型
type WDCallback = Parameters<typeof walletDetector>[1];

// 进一步提取回调参数中 newWallet 的类型
type Wallet = Parameters<WDCallback>[0]['newWallet'];

let wallet: Wallet | null = null;

const stop = walletDetector(connection, ({ newWallet }) => {
  wallet = newWallet;
  stop();
});
提取返回类型
import { unpackDelegation } from '@aeternity/aepp-sdk';

// 提取 unpackDelegation 的返回类型
type DlgUnpacked = ReturnType<typeof unpackDelegation>;

let delegation: DlgUnpacked | null = null;
delegation = unpackDelegation('ba_+EYDAaEB...');
初始化特定类型的参数

定义对象参数时,TypeScript 可能会泛化字符串类型。例如:

// 问题:txHash 被推断为 string 而不是 th_${string}
const gaAuthData = {
  tag: EntryTag.GaMetaTxAuthData,
  fee: 766e11,
  txHash: 'th_2CKnN6EorvNiwwqRjSzXLrPLiHmcwo4Ny22dwCrSYRoD6MVGK1',
};

// 这会报错!
const gaAuthDataPacked = packEntry(gaAuthData);
解决方案 1:显式定义类型
import { Tag, Encoded } from '@aeternity/aepp-sdk';

interface GaAuthData {
  tag: Tag;
  fee: number;
  txHash: Encoded.TxHash;
}

const gaAuthData: GaAuthData = {
  tag: EntryTag.GaMetaTxAuthData,
  fee: 766e11,
  txHash: 'th_2CKnN6EorvNiwwqRjSzXLrPLiHmcwo4Ny22dwCrSYRoD6MVGK1',
};
解决方案 2:使用 as const
// 使用 as const 将类型收窄为字面量类型
const gaAuthData = {
  tag: EntryTag.GaMetaTxAuthData,
  fee: 766e11,
  txHash: 'th_2CKnN6EorvNiwwqRjSzXLrPLiHmcwo4Ny22dwCrSYRoD6MVGK1',
} as const;
收窄联合类型

unpackTxunpackDelegation 等方法返回多种类型的联合。需要收窄类型后才能访问特定字段:

import { unpackTx, Tag } from '@aeternity/aepp-sdk';

const encodedTx = 'tx_+F0MAaEB4TK48d23oE5jt/qWR5pUu8UlpTGn8bwM5JISGQMGf7Ch...';
const tx = unpackTx(encodedTx);

// 方式 1:检查 tag 属性
if (tx.tag !== Tag.SpendTx) {
  throw new Error(`Unknown transaction type: ${Tag[tx.tag]}`);
}
// 现在 TypeScript 知道 tx 是 SpendTx
console.log(tx.amount);

// 方式 2:在 unpackTx 第二个参数指定类型
const spendTx = unpackTx(encodedTx, Tag.SpendTx);
console.log(spendTx.amount); // 直接可用
注意:不要只使用泛型参数 unpackTx<Tag.SpendTx>(encodedTx),这样 JavaScript 运行时不会验证交易类型。
验证用户输入

用户输入的地址是 string 类型,但 SDK 方法需要 Encoded.AccountAddress

import { isEncoded, Encoding } from '@aeternity/aepp-sdk';

const address: string = getUserInput(); // 用户输入

// 验证地址格式
if (!isEncoded(address, Encoding.AccountAddress)) {
  alert('地址格式无效');
  return;
}

// isEncoded 通过后,TypeScript 自动将 address 视为 ak_${string}
await aeSdk.spend(100, address); // 类型安全!
验证 AENS 名称
import { isName, ensureName, Name } from '@aeternity/aepp-sdk';

// 检查是否为有效名称
console.log(isName('myname.chain')); // true
console.log(isName('мир.chain'));    // true (支持国际化)
console.log(isName('🙂.chain'));     // false (不支持 emoji)

// 或使用 ensureName(无效时抛出异常)
const nameAsString: string = readName();
ensureName(nameAsString); // 如果无效会抛出
const name = new Name(nameAsString, options);
合约方法类型检查

通过泛型参数为 Contract 实例启用方法类型检查:

import { Contract, ContractMethodsBase } from '@aeternity/aepp-sdk';

// 定义合约接口
interface FooContract extends ContractMethodsBase {
  foo: (v: bigint) => bigint;
  bar: (x: Map<string, bigint>) => Map<string, bigint>;
  baz: (v: { FirstName: [string] } | { LastName: [string] }) => number;
}

// 使用接口创建合约
const contract = await Contract.initialize<FooContract>({
  ...aeSdk.getContext(),
  sourceCode,
});

await contract.$deploy([]);

// 现在有完整的类型提示和检查
console.log((await contract.foo(21n)).decodedResult);  // bigint
console.log((await contract.bar(new Map([['test', 10n]]))).decodedResult);
console.log((await contract.baz({ FirstName: ['Nikita'] })).decodedResult);