TypeScript 中的原子类型区分(名义原子类型) [英] Atomic type discrimination (nominal atomic types) in TypeScript

查看:31
本文介绍了TypeScript 中的原子类型区分(名义原子类型)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我只是好奇,有没有办法在 TypeScript 中区分原子类型以提高类型安全性?

I'm just curios, is there a way to discriminate atomic types for greater type safety in TypeScript?

换句话说,有没有办法复制下面的行为:

In other words, is there a way to replicate behavior below:

export type Kilos<T> = T & { discriminator: Kilos<T> };   // or something else  
export type Pounds<T> = T & { discriminator: Pounds<T> }; // or something else

export interface MetricWeight {
    value: Kilos<number>
}

export interface ImperialWeight {
    value: Pounds<number>
}

const wm: MetricWeight = { value: 0 as Kilos<number> }
const wi: ImperialWeight = { value: 0 as Pounds<number> }

wm.value = wi.value;                  // Should give compiler error
wi.value = wi.value * 2;              // Shouldn't error, but it's ok if it would, because it would require type casting which asks for additional attention
wm.value = wi.value * 2;              // Already errors
const we: MetricWeight = { value: 0 } // Already errors

或者可以把它放在一个容器里的东西:

Or something that would allow to put it in one container:

export type Discriminate<T> = ...

export type Kilos<T> = Discriminate<Kilos<T>>;
export type Pounds<T> = Discriminate<Pounds<T>>;

...

编辑

好的,事实证明可以使用 ZpdDG4gta 在这里发现的不可能的类型黑客来构建这样的类型 https://github.com/microsoft/TypeScript/issues/202

Ok, it turns out it's possible to build such type using impossible type hack discovered by ZpdDG4gta here https://github.com/microsoft/TypeScript/issues/202

但是现在的语言版本有点乱:

But it's a bit messy with current language version:

export type Kilos<T> = T & { discriminator: any extends infer O | any ? O : never };
export type Pounds<T> = T & { discriminator: any extends infer O | any ? O : never };

export interface MetricWeight {
    value: Kilos<number>
}

export interface ImperialWeight {
    value: Pounds<number>
}

const wm: MetricWeight = { value: 0 as Kilos<number> }
const wi: ImperialWeight = { value: 0 as Pounds<number> }

wm.value = wi.value;                       // Errors, good
wi.value = wi.value * 2;                   // Errors, but it's +/- ok
wi.value = wi.value * 2 as Pounds<number>; // Shouldn't error, good
wm.value = wi.value * 2;                   // Errors, good
const we: MetricWeight = { value: 0 }      // Errors, good

不幸的是,以下方法不起作用:

Unfortunately the following wouldn't work:

export type Discriminator<T> = T & { discriminator: any extends infer O | any ? O : never } 

export type Kilos<T> = Discriminator<T>;
export type Pounds<T> = Discriminator<T>;

export interface MetricWeight {
    value: Kilos<number>
}

export interface ImperialWeight {
    value: Pounds<number>
}

const wm: MetricWeight = { value: 0 as Kilos<number> }
const wi: ImperialWeight = { value: 0 as Pounds<number> }

wm.value = wi.value;                       // Doesn't error, this is bad
wi.value = wi.value * 2;                   // Errors, but it's +/- ok
wi.value = wi.value * 2 as Pounds<number>; // Shouldn't error, good
wm.value = wi.value * 2;                   // Errors, good
const we: MetricWeight = { value: 0 }      // Errors, good

编辑

事实证明,根据@jcalz,还有另一种方法可以引入不可能的类型:

It turns out that there is another way to introduce the impossible type, as per @jcalz:

export type Kilos<T> = T & { readonly discriminator: unique symbol };
export type Pounds<T> = T & { readonly discriminator: unique symbol };

...

但是仍然存在缺乏

export type Discriminator<T> = ...

有什么想法可以让它更干净吗?由于类型别名使两个类型引用都坚持鉴别器...

Any thoughts to make it cleaner? Since type aliasing makes both type references stick to Discriminator...

编辑

进一步的优化表明,可以将区分类型定义为:

Further optimization shown that it's possible to define discriminated type as:

export type Kilos<T> = T & { readonly '': unique symbol };
export type Pounds<T> = T & { readonly '': unique symbol };

有助于解决IDE的智能感知污染

Which helps with resolution of IDE's intellisense pollution

推荐答案

就这样定义:

const marker = Symbol();

export type Kilos = number & { [marker]?: 'kilos' };
export const Kilos = (value = 0) => value as Kilos;

export type Pounds = number & { [marker]?: 'pounds' };
export const Pounds = (value = 0) => value as Pounds;

然后磅和公斤会自动转换为数字和数字,但不会相互转换.

Then Pounds and Kilos are auto casted on numbers and from numbers, but not on each others.

let kilos = Kilos(0);
let pounds = Pounds(0);
let wrong: Pounds = Kilos(20); // Error: Type 'Kilos' is not assignable to type 'Pounds'.

kilos = 10; // OK
pounds = 20;  // OK

let kilos2 = 20 as Kilos; // OK
let kilos3: Kilos = 30; // OK

pounds = kilos;  // Error: Type 'Kilos' is not assignable to type 'Pounds'.
kilos = pounds; // Error: Type 'Pounds' is not assignable to type 'Kilos'.

kilos = Kilos(pounds / 2); // OK
pounds = Pounds(kilos * 2); // OK

kilos = Pounds(pounds / 2); // Error: Type 'Pounds' is not assignable to type 'Kilos'.

kilos = pounds / 2; // OK
pounds = kilos * 2; // OK

如果您想阻止自动转换增强"单位到普通"number 然后从标记字段中删除 optional :

If you want to prevent auto cast from "enhanced" unit to "plain" number then just remove optional from marker field:

const marker = Symbol();
export type Kilos = number & { [marker]: 'kilos' };
// ------------------------------------^ -?
export const Kilos = (value = 0) => value as Kilos;

// then:
const kilos = Kilos(2); // OK
kilos = 2; // Error
kilos = kilos * 2; // Error

这篇关于TypeScript 中的原子类型区分(名义原子类型)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆