benathon
benathon

Reputation: 7633

Prevent typescript conversion for identical types

In my typescript program I have two coordinate systems, that are convertible to each-other. They are both x,y but the have different scales. I would like to setup typescript so that it will warn me if I pass the wrong type to a function.

type Vec2 = [number,number]; // the base type

interface TileC extends Vec2 {};  // the two types I would like to be exclusive
interface ChunkC extends Vec2 {}; // the two types I would like to be exclusive

// example functions

let chunkSize = 32;

function tileToChunk(t: TileC): ChunkC {
  const [x,y] = t;
  const c: Vec2 = [Math.floor(x/chunkSize), Math.floor(y/chunkSize)];
  return c;
}

function chunkToTile(c: ChunkC): TileC {
  const [cx,cy] = c;
  return [cx*chunkSize, cy*chunkSize];
}

I would like the following to be an error

let chunkCoord: ChunkC = tileToChunk([7,14]);
let wantError = tileToChunk(chunkCoord);

As it stands, this compiles just fine. Am I missing a compiler option?

Upvotes: 1

Views: 170

Answers (3)

benathon
benathon

Reputation: 7633

Please see my update in the accepted answer.

After taking a further look into this. I can achieve what I want by doing this. Note that _type_lock can be any name, as long as it's the same name for both (all) similar types. "foo" and "bar" can be anything as long as they are unique. The name of the type is probably a good option here.

type Vec2 = [number,number]; // the base type

interface TileC extends Vec2 {
  _type_lock?: "foo";
};
interface ChunkC extends Vec2 {
  _type_lock?: "bar";
};

Upvotes: 1

benathon
benathon

Reputation: 7633

After more testing, my initial answer actually doesn't work. This version works. Features:

  • Allows the base to convert into the extended types
  • Does not let extended types convert to each-other
  • Does not let extended types convert to base (But can be done with a cast)
  • Nicer error messages
  • No effect on runtime / generated typescript

code:

// base type
type Vec2 = [number,number];

// lock helpers
interface LockTileC {
  readonly _typelock_tilec?: void;
}

interface LockChunkC {
  readonly _typelock_chunkc?: void;
}

// instanced types
type TileC  = [number,number,LockTileC?];
type ChunkC = [number,number,LockChunkC?];

// This option does it all in one line, but the error messages are worse
// type TileC = [number,number,{readonly _typelock_tilec?: void;}?];
// type ChunkC = [number,number,{readonly _typelock_chunkc?: void;}?];


function baseType(num: Vec2): void {
  console.log(`${num[0]}, ${num[1]}`);
}

function onlyTileC(num: TileC): void {
  console.log(`${num[0]}, ${num[1]}`);
}

function onlyChunkC(num: ChunkC): void {
  console.log(`${num[0]}, ${num[1]}`);
}


function example(): void {
  let asChunk: ChunkC = [1,2];
  let asTile:  TileC  = [32,64];
  let base:    Vec2   = [3,4];

  onlyTileC(asTile);        // OK:    type to type
  onlyTileC(asChunk);       // FAIL:  cross type to cross type
  onlyChunkC(asTile);       // FAIL:  cross type to cross type
  onlyTileC(base);          // OK:    base to type
  onlyChunkC(base);         // OK:    base to type
  baseType(asChunk);        // FAIL:  type to base
  baseType(<Vec2>asChunk);  // OK:    type to base with casting
}

Upvotes: 0

Christian Held
Christian Held

Reputation: 2828

You are not missing a compiler option, this is by design. TypeScript compiles to JavaScript and hence there is no static type information available at runtime.

TypeScript uses structural typing:

The basic rule for TypeScript’s structural type system is that x is compatible with y if y has at least the same members as x.

https://www.typescriptlang.org/docs/handbook/type-compatibility.html

Good news is you can use Discriminated Unions to achieve your desired behavior, for example by adding a kind property:

interface TileC {
    x: number;
    y: number;
    kind: "Tile"
}

interface ChunkC {
    x: number;
    y: number;
    kind: "Chunk"
}

interface VecC = TileC | ChunkC

Upvotes: 1

Related Questions