特殊
TypeScript 使用
类型安全的开发实践和常见问题
概述
本指南解释在 TypeScript 项目中使用 Æternity SDK 时可能遇到的边缘情况及处理方法。SDK 完全使用 TypeScript 编写,提供完整的类型定义。
提取 SDK 方法的类型
SDK 不单独导出类型以减少导出数量。使用 TypeScript 内置泛型 Parameters 和 ReturnType 提取类型:
提取参数类型
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;
收窄联合类型
unpackTx、unpackDelegation 等方法返回多种类型的联合。需要收窄类型后才能访问特定字段:
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);
继续学习