使用TypeScript中的基本类型创建唯一类型?

时间:2017-07-15 00:01:41

标签: typescript

我想给类对象提供唯一的id类型,即使它们是所有字符串。我尝试使用type,并尝试从具有唯一子类名称的基类派生。

请参阅以下示例。 typeextends都不允许我指示编译器将这些视为唯一类型。我仍然可以将HumanId传递给期望AnimalId的函数,反之亦然。

我认为他们是对象兼容的,而从底层的JavaScript角度来看,这是完全有道理的。事实上,如果我向AnimalId添加一个唯一的成员,我会得到我期望的错误:

Argument of type 'HumanId' is not assignable to parameter of type 'AnimalId'.

TypeScript是否有一种很好的方法可以为基本类型创建唯一的类型别名?

// type HumanId = string;
// type AnimalId = string;

class id {
    constructor(public value: string) { }
    toString(): string { return this.value;}
}
class HumanId extends id { };
class AnimalId extends id { };

function humanTest(id: HumanId): void {

}

function animalTest(id: AnimalId): void {

}

let h: HumanId = new HumanId("1");
let a: AnimalId = new AnimalId("2");

animalTest(h);

4 个答案:

答案 0 :(得分:3)

正如您所提到的,这些类型在结构上是兼容的。使它们唯一的唯一方法是为它们添加唯一属性。

如果您只想编译器来区分这两者,您可以添加虚拟唯一成员,这些成员不会产生运行时差异:

class HumanId extends id {
  private _humanId: HumanId; // Could be anything as long as it is unique from the other class
}
class AnimalId extends id {
  private _animalId: AnimalId;
}

答案 1 :(得分:1)

如果您希望这些数据类型在JSON中可序列化以用于API或数据库结构,则它们需要保留字符串基类型。有两种方法可以做到这一点。

(1)将ID类型实现为字符串文字

由于字符串文字是字符串,因此TypeScript主要做正确的事情。对于您的人类和动物示例,您可以创建以下字符串文字类型和函数来安全地创建/强制这些类型。

const setOfExclusiveIdsAlreadyCreated = new Set<string>();
const setOfExclusivePrefixesAlreadyCreated = new Set<string>();
const createMutuallyExclusiveId = <ID_TYPE extends string>(idType: ID_TYPE, idPrefix: string = "") => {
  // Ensure we never create two supposedly mutually-exclusive IDs with the same type
  // (which would, then, not actually be exclusive).
  if (setOfExclusiveIdsAlreadyCreated.has(idType)) {
    throw Error("Each type of ID should have a unique ID type");
  }
  // If setting a prefix, make sure that same prefix hasn't been used by
  // another type.
  setOfExclusiveIdsAlreadyCreated.add(idType);
  if (idPrefix && idPrefix.length > 0) {
    if (setOfExclusivePrefixesAlreadyCreated.has(idPrefix)) {
      throw Error("If specifying a prefix for an ID, each Id should have a unique prefix.");
    }
    setOfExclusiveIdsAlreadyCreated.add(idPrefix);
  }
  return (idToCoerce?: string) =>
    (typeof(idToCoerce) === "string") ?
      // If a string was provided, coerce it to this type
      idToCoerce as ID_TYPE :
      // If no string was provided, create a new one.  A real implementation
      // should use a longer, better random string
      (idPrefix + ":" + Math.random().toString()) as ID_TYPE;
}

//
// Create our human type and animal types
//

// The human type will appear to typescript to always be the literal "[[[Human]]]"
const HumanId = createMutuallyExclusiveId("[[[HumanId]]]", "human");
type HumanId = ReturnType<typeof HumanId>;
// The animal type will appear to typescript to always be the literal "[[[Animal]]]"
const AnimalId = createMutuallyExclusiveId("[[[AnimalId]]]", "animal");
type AnimalId = ReturnType<typeof AnimalId>;

const firstHumanId: HumanId = HumanId("Adam");
const randomlyGeneratedHumanId = HumanId();
const firstAnimalId = AnimalId("Snake");

// You can copy human types from one to the other
const copyOfAHumanId: HumanId = firstHumanId;
const anotherCopyOfAHumanId: HumanId = randomlyGeneratedHumanId;

// You CANNOT assign a human type to an animal type.
const animalId: AnimalId = firstHumanId; // type error

// You can pass an animal to a function that takes animals.
( (animal: AnimalId) => { console.log("The animal is " + animal) } )(firstAnimalId);

// You CANNOT pass a human to a function that takes animals.
( (animal: AnimalId) => { console.log("The animal is " + animal) } )(firstHumanId); // type error


interface Animal { animalId: AnimalId, makeSound: () => void };

const poodleId = AnimalId("poodle");
const animals: {[key in AnimalId]: Animal} = {
  [poodleId]: {
    animalId: poodleId,
    makeSound: () => { console.log("yip!"); }
  }
};

(2)将ID类型实现为枚举

// The human type will appear to typescript to be an enum
enum HumanIdType { idMayOnlyRepresentHuman = "---HumanId---" };
// Since enums are both types and consts that allow you to
// enumerate the options, we only want the type part, we
// export only the type.
export type HumanId = HumanIdType;

// Do the same for animals
enum AnimalIdType { idMayOnlyRepresentAnimal = "---AnimalId---" };
export type AnimalId = AnimalIdType;

const firstHumanId = "Adam" as HumanId;
const firstAnimalId = "Snake" as AnimalId;

// You can copy human types from one to the other
const copyOfAHumanId: HumanId = firstHumanId;

// You CANNOT assign a human type to an animal type.
const animalId: AnimalId = firstHumanId; // type error

// You can pass an animal to a function that takes animals.
( (animal: AnimalId) => { console.log("The animal is " + animal) } )(firstAnimalId);

// You CANNOT pass a human to a function that takes animals.
( (animal: AnimalId) => { console.log("The animal is " + animal) } )(firstHumanId); // type error


interface Animal { animalId: AnimalId, makeSound: () => void };

const poodleId = "poodle" as AnimalId;
const animals: {[key in AnimalId]: Animal} = {
  [poodleId]: {
    animalId: poodleId,
    makeSound: () => { console.log("yip!"); }
  }
};

// In order for JSON encoding/decoding to just work,
// it's important that TypeScript considers enums
// of this form (those with only one field that i
// represented as a string) as basic strings.
const animalIdsAreAlsoStrings: string = poodleId;

两种实施方式的注意事项

您需要避免键入用作地图的对象,如下所示:

const animalIdToAnimalMap: {[key: AnimalId]: Animal} = {};

而是喜欢这样做:

const animalIdToAnimalMap: {[key in AnimalId]?: Animal} = {}

尽管如此,一些打字仍然不能完美运作。例如,如果使用Object.entries(animalIdToAnimalMap),则键将是字符串而不是AnimalId(这将是typeof animalof typeIdToAnimalMap的类型)。

希望TypeScript在未来提供互斥的ID并改进Object.entries等函数的默认类型。在那之前,我希望这仍然有帮助。这些方法确实帮助我避免了一些错误,否则我会以错误的顺序传递id或者混淆不同类型的id。

答案 2 :(得分:1)

我遇到了这个问题,但我的用例有一个小变化:我想为 number 引入一个独特的类型。想想您拥有的 API,例如hours: numberminutes: numberseconds: number 等,但您希望类型系统强制正确使用所有单位。

@Evert 提到的 blog post 在这方面是一个很好的资源。这个想法是创建一个带有一些从未实际使用过的虚拟对象的交叉类型。创建新的唯一类型可以通过通用助手类型抽象出来。举例说明:

// Generic definition somewhere in utils
type Distinct<T, DistinctName> = T & { __TYPE__: DistinctName };

// Possible usages
type Hours = Distinct<number, "Hours">;
type Minutes = Distinct<number, "Minutes">;
type Seconds = Distinct<number, "Seconds">;

function validateHours(x: number): Hours | undefined {
  if (x >= 0 && x <= 23) return x as Hours;
}
function validateMinutes(x: number): Minutes | undefined {
  if (x >= 0 && x <= 59) return x as Minutes;
}
function validateSeconds(x: number): Seconds | undefined {
  if (x >= 0 && x <= 59) return x as Seconds;
}

现在函数 f(h: Hours, m: Minutes, s: Seconds) 不能只用任何 number 调用,但可以确保完全类型安全。另请注意,该解决方案没有内存/运行时开销。

在实践中,这种方法对我很有效,因为这些“不同”类型可以在需要 number 的地方隐式使用。通过例如显式转换as Hour 只需要反过来。一个小缺点是像 hours += 1 这样的表达式需要替换为 hours = (hours + 1) as Hours。正如博文中所展示的那样,好处通常会超过稍微更明确的语法。

旁注:我将我的泛型类型称为 Distinct 是因为我觉得这个名字更自然,这就是 Nim 编程语言中的特性调用方式。

答案 3 :(得分:0)

这是另一个探索的想法。您可以通过泛型来定义唯一的Id类型,而不是继承:

class Id<Entity> {
  constructor(value) {
    this.value = value;
  }

  equals(id: Id<Entity>) {
    return this.value === id.value;
  }

  public readonly value;
  // This member exists for the sole purpose of making
  // one Id class structurally different to another
  private readonly _: Entity;  
}


class Animal {
  id: Id<Animal>;
  growl() {}
}

class Human {
  id: Id<Human>;
  makeCoffee() {}
}

const animal = new Animal();
animal.id = new Id<Animal>("3");

const human = new Human();
// This triggers a compilation error.
human.id = animal.id;

human.id = new Id<Human>("3");
// Another compilation error
const humanIsAnimal = human.id.equals(animal.id);

// Demonstrating compatibility
const animal2 = new Animal();
animal2.id = animal.id;
const animalsMatch = animal2.id.equals(animal.id);