Asked  5 Months ago    Answers:  5   Viewed   25 times

Title says it all - why doesn't Object.keys(x) in TypeScript return the type Array<keyof typeof x>? That's what Object.keys does, so it seems like an obvious oversight on the part of the TypeScript definition file authors to not make the return type simply be keyof T.

Should I log a bug on their GitHub repo, or just go ahead and send a PR to fix it for them?

 Answers

71

The current return type (string[]) is intentional. Why?

Consider some type like this:

interface Point {
    x: number;
    y: number;
}

You write some code like this:

function fn(k: keyof Point) {
    if (k === "x") {
        console.log("X axis");
    } else if (k === "y") {
        console.log("Y axis");
    } else {
        throw new Error("This is impossible");
    }
}

Let's ask a question:

In a well-typed program, can a legal call to fn hit the error case?

The desired answer is, of course, "No". But what does this have to do with Object.keys?

Now consider this other code:

interface NamedPoint extends Point {
    name: string;
}

const origin: NamedPoint = { name: "origin", x: 0, y: 0 };

Note that according to TypeScript's type system, all NamedPoints are valid Points.

Now let's write a little more code:

function doSomething(pt: Point) {
    for (const k of Object.keys(pt)) {
        // A valid call iff Object.keys(pt) returns (keyof Point)[]
        fn(k);
    }
}
// Throws an exception
doSomething(origin);

Our well-typed program just threw an exception!

Something went wrong here! By returning keyof T from Object.keys, we've violated the assumption that keyof T forms an exhaustive list, because having a reference to an object doesn't mean that the type of the reference isn't a supertype of the type of the value.

Basically, (at least) one of the following four things can't be true:

  1. keyof T is an exhaustive list of the keys of T
  2. A type with additional properties is always a subtype of its base type
  3. It is legal to alias a subtype value by a supertype reference
  4. Object.keys returns keyof T

Throwing away point 1 makes keyof nearly useless, because it implies that keyof Point might be some value that isn't "x" or "y".

Throwing away point 2 completely destroys TypeScript's type system. Not an option.

Throwing away point 3 also completely destroys TypeScript's type system.

Throwing away point 4 is fine and makes you, the programmer, think about whether or not the object you're dealing with is possibly an alias for a subtype of the thing you think you have.

The "missing feature" to make this legal but not contradictory is Exact Types, which would allow you to declare a new kind of type that wasn't subject to point #2. If this feature existed, it would presumably be possible to make Object.keys return keyof T only for Ts which were declared as exact.


Addendum: Surely generics, though?

Commentors have implied that Object.keys could safely return keyof T if the argument was a generic value. This is still wrong. Consider:

class Holder<T> {
    value: T;
    constructor(arg: T) {
        this.value = arg;
    }

    getKeys(): (keyof T)[] {
        // Proposed: This should be OK
        return Object.keys(this.value);
    }
}
const MyPoint = { name: "origin", x: 0, y: 0 };
const h = new Holder<{ x: number, y: number }>(MyPoint);
// Value 'name' inhabits variable of type 'x' | 'y'
const v: "x" | "y" = (h.getKeys())[0];

or this example, which doesn't even need any explicit type arguments:

function getKey<T>(x: T, y: T): keyof T {
    // Proposed: This should be OK
    return Object.keys(x)[0];
}
const obj1 = { name: "", x: 0, y: 0 };
const obj2 = { x: 0, y: 0 };
// Value "name" inhabits variable with type "x" | "y"
const s: "x" | "y" = getKey(obj1, obj2);
Tuesday, June 1, 2021
 
laurent
answered 5 Months ago
100

As of TypeScript 2.* the 'tsconfig.json' has the following two properties available:

{
    'typeRoots': [],
    'types': [] 
}

I'll detail both in order.


  1. 'typeRoots' specifies root folders in which the transpiler should look for type definitions (eg: 'node_modules').

    • If you've been using typescript, you know that for different libraries that have not been written using typescript, you need definitions in order for the compiler to recognize global variables and to have IntelliSense support.

    • This issue has been tackled by projects (repos) such as 'DefinatelyTyped' that use tools such as tsd or typings module to download typings required for your project, but they also come with their own 'json' file that needs to be maintained separately.

    • With TS2.* you can now fetch definition dependencies using 'npm'. So instead of using a seperate cli library like tsd or typings, you can now just use: npm i @types/{LIB} this way, all dependencies are managed using package.json and you can easily eliminate the necessity of another 'json' file to maintain in your project.


  1. 'types' are the actual library names that will be found in the typeRoot.

    • so let's say you have the default configuration for typeRoots which would look something like:

      "typeRoots": [
          "./node_modules/@types"
      ]
      
    • let's say you now want to use jasmine as a test framework for your project, so you have your typeRoot folder configured, all you have too do now is execute: npm i @types/jasmine --save-dev

    • after the definition package is installed you just need to configure your 'types' property in 'tsconfig.json' as follows:

      "types": [
           "core-js",
           "jasmine",
           "requirejs",
           "chance"
      ]
      

To conclude, basically you tell the TS compiler the following:

typeRoots: You need to look for typings in these folders. types: In one of the folders provided above, you will find definitions for theses framworks (subfolders).

So using the scenario above, and if we add another root:

"typeRoots": [
    "./node_modules/@types",
    "./custom_definitions"
],
"types": [
    "jasmine",
]

TS will now look for definition files in

./node_modules/@types/jasmine

or

./custom_definitions/jasmine

Hope this helps!

Thursday, June 10, 2021
 
Manmay
answered 5 Months ago
84

This behavior is going away.

Starting at whatever release comes after TypeScript 1.8 (or right now if you're using the bleeding-edge compiler), you will no longer see this error when the originating expression for a type is an object literal.

See https://github.com/Microsoft/TypeScript/pull/7029


The Old Answer for The Old Compilers

Index signatures and object literals behave specially in TypeScript. From spec section 4.5, Object Literals:

When an object literal is contextually typed by a type that includes a string index signature of type T, the resulting type of the object literal includes a string index signature with the widened form of the best common type of T and the types of the properties declared in the object literal.

What does this all mean?

Contextual Typing

Contextual typing occurs when the context of an expression gives a hint about what its type might be. For example, in this initialization:

var x: number = y;

The expression y gets a contextual type of number because it's initializing a value of that type. In this case, nothing special happens, but in other cases more interesting things will occur.

One of the most useful cases is functions:

// Error: string does not contain a function called 'ToUpper'
var x: (n: string) => void = (s) => console.log(s.ToUpper());

How did the compiler know that s was a string? If you wrote that function expression by itself, s would be of type any and there wouldn't be any error issued. But because the function was contextually typed by the type of x, the parameter s acquired the type string. Very useful!

Index Signatures

An index signature specifies the type when an object is indexed by a string or a number. Naturally, these signatures are part of type checking:

var x: { [n: string]: Car; };
var y: { [n: string]: Animal; };
x = y; // Error: Cars are not Animals, this is invalid

The lack of an index signature is also important:

var x: { [n: string]: Car; };
var y: { name: Car; };
x = y; // Error: y doesn't have an index signature that returns a Car

Hopefully it's obvious that the above two snippets ought to cause errors. Which leads us to...

Index Signatures and Contextual Typing

The problem with assuming that objects don't have index signatures is that you then have no way to initialize an object with an index signature:

var c: Car;
// Error, or not?
var x: { [n: string]: Car } = { 'mine': c };

The solution is that when an object literal is contextually typed by a type with an index signature, that index signature is added to the type of the object literal if it matches. For example:

var c: Car;
var a: Animal;
// OK
var x: { [n: string]: Car } = { 'mine': c };
// Not OK: Animal is not Car
var y: { [n: string]: Car } = { 'mine': a };

Putting It All Together

Let's look at the original functions in the question:

function a(): StringMap {
    return { a: "1" }; // OK
}

OK, because expressions in return statements are contextually typed by the return type of the function. The object literal {a: "1"} has a string value for its sole property, so the index signature can be successfully applied.

function b(): StringMap {
    var result: StringMap = { a: "1" };
    return result; // OK
}

OK, because the initializers of explicitly-typed variables are contextually typed by the type of the variable. As before, the index signature is added to the type of the object literal.

function c(): StringMap {
    var result = { a: "1" };
    return result; // Error - result lacks index signature, why?
}

Not OK, because result's type does not have an index signature.

Thursday, July 29, 2021
 
Akdeniz
answered 3 Months ago
73
class Apple {
  appleFoo: any;
}

class Orange {
  orangeFoo: any;
}

var arr : Array<Apple|Orange> = [];

var apple = new Apple();
var orange = new Orange();

arr.push(apple); //ok
arr.push(orange); //ok
arr.push(1); //error
arr.push("abc"); // error

var something = arr[0];

if(something instanceof Apple) {
  something.appleFoo; //ok
  something.orangeFoo; //error
} else if(something instanceof Orange) {
  something.appleFoo; //error
  something.orangeFoo; //ok
}
Sunday, August 15, 2021
 
Andras Zoltan
answered 3 Months ago
23

@NitzanTomer is right. It's not the match functions parameter that has a type mismatch. You certainly try to store the return value of the match function in a string typed variable / return it from a :string function / pass it as a string parameter.

Tuesday, August 24, 2021
 
Eric
answered 2 Months ago
Only authorized users can answer the question. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :  
Share