Riccardo Perego
Riccardo Perego

Reputation: 487

Use of typeof in keyof types typescript

I have a class, containing a static and a non-static parameter:

class Test {
  s1 = "Hello"
  static s2 = 16
}

I then create 2 keyof types, one using typeof and one without:

type t1 = typeof Test[keyof typeof Test]
type t2 =  Test[keyof Test]

Type t1 is a union between Test | number while type t2 is string.

What is the actual difference between the two? Does typeof return the types of static methods + the class? Why? What is the reasoning?

Upvotes: 1

Views: 980

Answers (1)

Connor Low
Connor Low

Reputation: 7176

tl;dr

  • typeof Test as a type refers to the type of the value of Test (a class function).
  • Test as a type refers to the type of an instance of Test (an object that looks like this: { s1: 'hello' }).

typeof

typeof can do one of two things in TypeScript, depending on the context. From the handbook:

JavaScript already has a typeof operator you can use in an expression context:

// Prints "string"
console.log(typeof "Hello world");

TypeScript adds a typeof operator you can use in a type context to refer to the type of a variable or property:

let s = "hello";
let n: typeof s; 
//  ^ let n: string

The situation you have described is using the second typeof operator (existing in the type context).

Classes

TypeScript classes are a bit strange; every other declaration in TypeScript will result in a new type or a new object (almost, TypeScript enums are an exception). For example, you can't use a variable a type, nor a type as a variable:

const foo = 'bar';
const baz: foo = foo;
//         ^^^ 'foo' refers to a value, but is being used as a type here. 
//             Did you mean 'typeof foo'?

interface fizz {
  buzz: string
}
const fizzbuzz = fizz['buzz'];
//               ^^^^ 'fizz' only refers to a type, 
//                    but is being used as a value here.

Declaring a class is different because you end up with both a TypeScript type and a JavaScript class, where the type represents an instance of the class. In other words, you can do this:

class Foo {}

// As a JavaScript constructor:
const foo = new Foo(); 

// As a TypeScript type:
let bar: Foo;

// As both:
const baz: Foo = new Foo();

One more thing to note: in JavaScript, classes are just functions, and functions are just objects. Adding a static property to a TypeScript class compiles to look something like this in JavaScript:

// TypeScript 
//   class Foo { static bar = 'baz' }
// compiles to:
class Foo {
}
Foo.bar = 'baz';

typeof Class

Regardless of whether you use typeof in the expression context or the type context, you must always use it on a value type. In other words, you can't do:

type Abc = 'hi';
type Zyx = typeof Abc;
//                ^^^ 
// 'Abc' only refers to a type, but is being used as a value here.

Because of this, when we know that typeof Test is going to use the value of Test rather than its type; as you noted, this is different from the type, Test. So, what is the type of typeof Test?

Remember, JavaScript classes are just functions that can be used as constructors (try running console.log(typeof Test) for proof). typeof Class is going to give you a type that represents that constructor function with additional narrowing specific to that class. You could use it like so:

class Test {
  s1 = "Hello"
  static s2 = 16
}

type T = typeof Test;
const Foo: T = class Bar extends Test { }

// Because of duck typing, you can also do this:
class Test2 { 
  s1 = "Hello"
  static s2 = 32
}

const Fizz: T = class Buzz extends Test2 { }

In other words, Test as a type gives you the structure of an instance, while typeof Test as a type gives you the structure of the class itself.

Looking at the keyof behavior:

// The keys of an instance of test:
type Keys = keyof Test; // 's1'

// The keys of the Test class, which is just a function and so includes
// the inherited 'prototype' property:
type Keys2 = keyof typeof Test; // 'prototype' | 's2'

And finally:

type t1 = typeof Test[keyof typeof Test];
// -> typeof Test['prototype' | 's2'] -> Test | number
type t2 = Test[keyof Test]
// -> Test['s1'] -> string

Playground

Upvotes: 3

Related Questions