typescript - 制作复合类型打字稿的地图
问题描述
我正在翻译一些他们使用 Vector2 字典到字符串的 C# 代码:
我制作了一个简单的测试用例(参见 .NET fiddle 的此处)来尝试转换为 JS:
using System;
using System.Collections.Generic;
Dictionary<Vector2, string> hd = new()
{
{new(0,0), "Zero" },
{new(0,1), "{0, 1}"},
{new(1,0), "{1, 0}"},
{new(1,1), "{1, 1}"}
};
foreach (var (key, value) in hd)
Console.WriteLine($"Key: {{{key.x}, {key.y}}}. Value: {value}");
public record Vector2 (double x, double y);
我试图翻译成打字稿,我想制作一个通用的字典类型,它可以在任何类型上工作,你可以放置一个函数,该函数可以转换为你可以放入 JS 的值Map
。
我将转换器的哈希类型设置为可映射类型:
interface Hasher<T, Hash>
{
GetHashCode(value: T ): Hash;
GetValue (hash:Hash): T;
}
和 Vector2 类型:
interface Vector2 { x:number; y:number; }
我做了一个抽象类型如下:
abstract class Dictionary<TKey, TValue, Hash>
{
['constructor']: typeof Dictionary & Hasher<TKey, Hash>;
impl: Map<Hash, TValue> = new Map();
}
并做了一个简单的添加和迭代器:
Add(key: TKey, value: TValue)
{
this.impl.set(this.constructor.GetHashCode(key), value);
}
*[Symbol.iterator](): Iterator<[TKey, TValue]>
{
for (let [key, value] of this.impl)
yield [this.constructor.GetValue(key), value];
}
我做了一个函数来将 Vector2 转换为十六进制字符串:
static Vector2 = class<TValue> extends Dictionary<Vector2, TValue, string>
{
static GetHashCode(value: Vector2): string
{
let buffer = new ArrayBuffer(16);
let arr = new Float64Array(buffer);
arr[0] = value.x;
arr[1] = value.y;
return [...new Uint8Array(buffer)].map(x => x.toString(16).padStart(2, '0')).join('');
}
static GetValue(hash: string): Vector2
{
let buffer = new ArrayBuffer(16);
let u8arr = new Uint8Array(buffer);
for (let n = 0; n < 16; n++) u8arr[n] = +('0x'+hash.substr(n*2, 2));
let arr = new Float64Array(buffer);
return {x: arr[0], y: arr[1]};
}
}
我想添加一些类型检查以确保我的 Dictionary 实现正在实现所需的方法:
// Type Checks:
{
const Dictionary$string: Hasher<string, string> = Dictionary.string ;
const Dictionary$int: Hasher<number, number> = Dictionary.int ;
const Dictionary$Vector2:Hasher<Vector2, string> = Dictionary.Vector2;
}
我的问题是是否有更好的方法来进行这些类型检查(也许不仅仅是生成多余的 JS)。这是我能找到的最好方法。
解决方案
所问问题的答案可能是“您为验证这些static
属性的类型所做的任何其他事情都将比您已经在做的事情涉及更多,因此您不妨继续这样做”。
TypeScript 没有一种简单的方法来验证一个值是否可以分配给某种类型,而无需通常将其扩展到该类型。你想说类似的话
static Vector2 = class <TValue> extends Dictionary<Vector2, TValue, string> {
/* snip */
} verify Hasher<Vector2, string>;
whereverify
是类型断言的一些仅类型系统的替代方案,它不会影响Vector2
属性的推断类型,但如果推断的类型不可分配给Hasher<Vector2, string>
. 在microsoft/TypeScript#7481上存在一个长期未解决的问题,要求提供这样的功能,但目前它不存在。
您可以自己实现一些类似的东西,但它有一个(次要的)运行时组件,并且由于缺乏对microsoft/TypeScript#26242中要求的部分类型参数推断的语言支持而变得复杂。这是一种可能的方法:
static Vector2 = verify(null! as Hasher<Vector2, string>,
class <TValue> extends Dictionary<Vector2, TValue, string> {
/* snip */
}
);
verify
看起来像哪里
const verify = <T, U extends T>(dummy: T, val: U) => val;
因此编译器将检查 of 的类型val
以确保它可以分配给 of 的类型dummy
,并且它在val
不扩大它的情况下返回。从概念上讲,编译器没有理由需要该dummy
值,但是您希望编译器U
在让您指定的同时进行推断T
,并且对此没有直接支持。
无论如何,您可以定义和调用verify()
而不是创建Dictionary$Vector2
,但我不知道这是否值得。
所以这是基本的答案。
不过,正如我在评论中提到的那样,我认为我不会尝试Dictionary
按照您的方式实施。我发现的问题:
您所说的“哈希”代码实际上是一个标识符。哈希码并不意味着识别一条数据;您使用哈希码将事物快速拆分为存储桶,并承认可能不止一件事物最终会出现在同一个存储桶中。因为
Dictionary
如果不同的键曾经有相同的哈希码,那将是非常糟糕的。相反,这里的意图是您的标识符函数应该定义键相等的含义:当且仅当它们产生相同的标识符时,两个键才相等。我不会允许标识符是任何类型,而是倾向于选择
string
并坚持使用它。无论您选择什么,都需要与 进行比较===
,因为这或多或少是Map
关键平等的工作方式。我看不出有多大用处,
GetValue
甚至可能有害。TypeScript 使用结构化类型系统,您可以在其中Vector2
使用额外的属性进行扩展。的值interface Vector2WithCheese extends Vector2 { cheese: true }
也是 type 的值Vector2
;如果我在Vector2WithCheese
字典中输入 a,我希望它Vector2WithCheese
会出来;不是一些新创建的Vector2
。即使没有子类型,打破键上的引用相等性也可能会让用户感到惊讶,并且似乎不会让你买太多。相反,我建议在 inner 中同时保存原始键和值Map
。我对声明
constructor
类属性的代码有点警惕。我想这是为了解决缺乏强类型构造函数的问题,根据microsoft/TypeScript#3841。根据microsoft/TypeScript#34516,我不知道要求构造函数本身具有某些静态属性,尤其abstract
是不受支持的静态属性有多大帮助。确保整个类以相同的方式计算密钥相等性可能有一些好处,但我不知道围绕类的静态端强输入的复杂性是否值得。相反,我可能只是将 key-identifier 函数作为参数传递给类构造函数,并允许两个实例Dictionary
可能会以不同的方式比较密钥。然后,与其将 的子类存储
Dictionary
为自身的静态属性,不如Dictionary
创建常规子类,其中键标识符函数以特定方式设置。一旦你这样做了,就不需要对子类进行类型检查了。
代码可能如下所示:
class Dict<K, V> {
private map = new Map<string, [K, V]>()
constructor(public toIdString: (k: K) => string, entries?: Iterable<[K, V]>) {
if (entries) {
for (const [k, v] of entries) {
this.set(k, v);
}
}
}
set(k: K, v: V) {
this.map.set(this.toIdString(k), [k, v]);
return this;
}
get(k: K): V | undefined {
return this.map.get(this.toIdString(k))?.[1]
}
[Symbol.iterator](): Iterator<[K, V]> {
return this.map.values();
}
}
和
class Vector2Dict<V> extends Dict<Vector2, V> {
constructor(entries?: Iterable<[Vector2, V]>) {
super(v => "" + v.x + "," + v.y, entries);
}
}
您可以验证它的行为是否符合要求:
const hd = new Vector2Dict<string>([
[{ x: 0, y: 0 }, "Zero"],
[{ x: 0, y: 1 }, "{0, 1}"],
[{ x: 1, y: 0 }, "{1, 0}"],
[{ x: 1, y: 1 }, "{1, 1}"]
]);
for (const [key, value] of hd) {
console.log(`Key: {${key.x}, ${key.y}}. Value: ${value}`);
}
/*
Key: {0, 0}. Value: Zero
Key: {0, 1}. Value: {0, 1}
Key: {1, 0}. Value: {1, 0}
Key: {1, 1}. Value: {1, 1}
*/
console.log(hd.get({ x: 0, y: 0 })?.toUpperCase()); // ZERO
推荐阅读
- java - 如何将数据类型字符串迁移到布尔值?
- random - “Last In, Random Out”有更好的名字吗?
- background-color - 使用 Pine-Script v4 (Tradingview) 在蜡烛周围包裹背景颜色
- javascript - AJAX 请求有效负载未显示在控制台日志中
- ruby-on-rails - 导轨中止!ArgumentError:参数数量错误(给定 0,预期 1..2)
- laravel - 更新给定 ID 集合中的 `index` 属性
- javascript - 如何使整个图像比较滑块居中
- c# - 当玩家在 Unity2D 中使用 Cinemachine 跳跃时,如何让凸轮忽略 y 轴?
- python - 继承和dunder方法?
- r - 测量观察组之间值的“传播”