patterntypescriptCriticalCanonical
Why doesn't Object.keys return a keyof type in TypeScript?
Viewed 0 times
typescriptobjectreturnkeyskeyofwhydoesntype
Problem
Title says it all - why doesn't
Should I log a bug on their GitHub repo, or just go ahead and send a PR to fix it for them?
Object.keys(x) in TypeScript return the type Array? 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?
Solution
The current return type (
Consider some type like this:
You write some code like this:
Let's ask a question:
In a well-typed program, can a legal call to
The desired answer is, of course, "No". But what does this have to do with
Now consider this other code:
Note that according to TypeScript's type system, all
Now let's write a little more code:
Our well-typed program just threw an exception!
Something went wrong here!
By returning
Basically, (at least) one of the following four things can't be true:
Throwing away point 1 makes
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
Addendum: Surely generics, though?
Commentors have implied that
or this example, which doesn't even need any explicit type arguments:
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 if 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:
keyof Tis an exhaustive list of the keys ofT
- A type with additional properties is always a subtype of its base type
- It is legal to alias a subtype value by a supertype reference
Object.keysreturnskeyof 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 {
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(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(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);Code Snippets
interface Point {
x: number;
y: number;
}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");
}
}interface NamedPoint extends Point {
name: string;
}
const origin: NamedPoint = { name: "origin", x: 0, y: 0 };function doSomething(pt: Point) {
for (const k of Object.keys(pt)) {
// A valid call if Object.keys(pt) returns (keyof Point)[]
fn(k);
}
}
// Throws an exception
doSomething(origin);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];Context
Stack Overflow Q#55012174, score: 258
Revisions (0)
No revisions yet.