Christian Vincenzo Traina
Christian Vincenzo Traina

Reputation: 10384

How to force the type of an already declared variable in TypeScript?

I have a case where I know the type of a variable, but TypeScript doesn't. I can't use a type guard because I'm inside a switch-case, and because of a known TypeScript bug it can't be implemented.

This is the scenario:

function handleObject(obj: any) {
  let myObj: any;
  switch (typeof obj) {
    case 'number':
      myObj: number = obj;
      // use myObj as a number
    break;
    case 'string':
      myObj: string = obj;
      // use myObj as a string
    break;
    //etc...

  }
}

This is a simplified example, but there are tens of possible types. Obviously, I can't declare a different variable for each scenario only to assign a different type.

My idea was to declare a lot of variables like myObjString, myObjNumber, etc..., but it would sound like Hungarian notation, which I don't like at all.

In the above example, the error that I get is:

'number' only refers to a type, but is being used as a value here.(2693)

Because the variable was already declared.

If instead I use myObj = obj as number or myObj = <number>obj it compiles, but myObj remains any.

How could I get the variable typed?

Upvotes: 1

Views: 2132

Answers (2)

Christian Vincenzo Traina
Christian Vincenzo Traina

Reputation: 10384

@MatthewLayton's answer is exemplary. I've anyway found a simpler way to force the type of an already declared variable. Using curly brackets around a case of a switch-case is enough to create a new block-scope:

function handleObject(obj: any) {
  switch (typeof obj) {
    case 'number': {
      const myObj: number = obj;
      // use myObj as a number
      break;
    }
    case 'string': {
      const myObj: string = obj;
      // use myObj as a string
      break;
      //etc...
    }

  }
}

Upvotes: 0

Matthew Layton
Matthew Layton

Reputation: 42260

You could pass a map of type constructors to functions into your handleObject function, which applies the Open/Closed Principle because you don't need to modify your handleObject function whenever you want to support new object types; rather, you just apply the type and the handler function to the map and pass the map to the function.

First, let's define a type that represents a type constructor:

type Ctor<T = unknown> = { new(...args: any[]): T };

We can also define a type to represent the map:

type TypedMap<T> = Map<Ctor<T>, (value: T) => void>;

Now let's refactor the handleObject function to work with an extensible map:

function handleObject(obj: any, map: TypedMap<any>) {
    if (map.has(obj.constructor)) {
        const fn = map.get(obj.constructor)!;
        fn(obj);
    }
}

Let's add a test class:

class Foo { }

Let's create a map that we can pass to our function:

const map: TypedMap<any> = new Map();
map.set(Number, (value: number) => console.log("It's a Number"));
map.set(String, (value: string) => console.log("It's a String"));
map.set(Foo, (value: Foo) => console.log("It's a Foo"));

And finally, let's test that it works:

handleObject("hello", map); // It's a String
handleObject(123, map); // It's a Number
handleObject(new Foo(), map); // It's a Foo

Altogether now:

type Ctor<T = unknown> = { new(...args: any[]): T };
type TypedMap<T> = Map<T, (value: T) => void>;

function handleObject(obj: any, map: TypedMap<any>) {
    if (map.has(obj.constructor)) {
        const fn = map.get(obj.constructor)!;
        fn(obj);
    }
}

class Foo {
}

const map: TypedMap<any> = new Map();
map.set(Number, (value: number) => console.log("It's a Number"));
map.set(String, (value: string) => console.log("It's a String"));
map.set(Foo, (value: Foo) => console.log("It's a Foo"));

handleObject("hello", map);
handleObject(123, map);
handleObject(new Foo(), map);

It might be a bit rough around the edges, but it seems to work nicely.

Upvotes: 1

Related Questions