Why doesn't Object.keys return a keyof type in TypeScript?












12















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?










share|improve this question























  • I opened and closed a PR today related to this topic. My PR was only focusing on the case where keys are coming from an enum of strings. In this precise case, it does not seem that inheritance is feasible. I need to double check before reopening it github.com/Microsoft/TypeScript/pull/30228

    – DubZzz
    2 hours ago
















12















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?










share|improve this question























  • I opened and closed a PR today related to this topic. My PR was only focusing on the case where keys are coming from an enum of strings. In this precise case, it does not seem that inheritance is feasible. I need to double check before reopening it github.com/Microsoft/TypeScript/pull/30228

    – DubZzz
    2 hours ago














12












12








12








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?










share|improve this question














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?







typescript






share|improve this question













share|improve this question











share|improve this question




share|improve this question










asked 3 hours ago









Ryan CavanaughRyan Cavanaugh

100k27171182




100k27171182













  • I opened and closed a PR today related to this topic. My PR was only focusing on the case where keys are coming from an enum of strings. In this precise case, it does not seem that inheritance is feasible. I need to double check before reopening it github.com/Microsoft/TypeScript/pull/30228

    – DubZzz
    2 hours ago



















  • I opened and closed a PR today related to this topic. My PR was only focusing on the case where keys are coming from an enum of strings. In this precise case, it does not seem that inheritance is feasible. I need to double check before reopening it github.com/Microsoft/TypeScript/pull/30228

    – DubZzz
    2 hours ago

















I opened and closed a PR today related to this topic. My PR was only focusing on the case where keys are coming from an enum of strings. In this precise case, it does not seem that inheritance is feasible. I need to double check before reopening it github.com/Microsoft/TypeScript/pull/30228

– DubZzz
2 hours ago





I opened and closed a PR today related to this topic. My PR was only focusing on the case where keys are coming from an enum of strings. In this precise case, it does not seem that inheritance is feasible. I need to double check before reopening it github.com/Microsoft/TypeScript/pull/30228

– DubZzz
2 hours ago












1 Answer
1






active

oldest

votes


















12














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.






share|improve this answer





















  • 1





    However, there is common case when point 3. is excluded, when for example T is inferred and is guaranteed to be precise: const f: <T>(t: T) => void = (t) => { Object.keys(t).forEach(k => t[k]) }. I have lots of places like that in my code, where I really want Object.keys() to return (keyof T).

    – artem
    3 hours ago













  • As arthem also points out, the confusion comes from the fact that 9 out of 10 times you will end up in some way using a type assertion to keyof T to do anything useful with the result of keys. You might argue it is better to be explicit about it so you are more aware of the risk you are taking, but probably 9/10 devs will just add the type assertion and not be aware of the issues you highlight ..

    – Titian Cernicova-Dragomir
    1 hour ago













Your Answer






StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");

StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "1"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);

StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});

function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});


}
});














draft saved

draft discarded


















StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f55012174%2fwhy-doesnt-object-keys-return-a-keyof-type-in-typescript%23new-answer', 'question_page');
}
);

Post as a guest















Required, but never shown

























1 Answer
1






active

oldest

votes








1 Answer
1






active

oldest

votes









active

oldest

votes






active

oldest

votes









12














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.






share|improve this answer





















  • 1





    However, there is common case when point 3. is excluded, when for example T is inferred and is guaranteed to be precise: const f: <T>(t: T) => void = (t) => { Object.keys(t).forEach(k => t[k]) }. I have lots of places like that in my code, where I really want Object.keys() to return (keyof T).

    – artem
    3 hours ago













  • As arthem also points out, the confusion comes from the fact that 9 out of 10 times you will end up in some way using a type assertion to keyof T to do anything useful with the result of keys. You might argue it is better to be explicit about it so you are more aware of the risk you are taking, but probably 9/10 devs will just add the type assertion and not be aware of the issues you highlight ..

    – Titian Cernicova-Dragomir
    1 hour ago


















12














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.






share|improve this answer





















  • 1





    However, there is common case when point 3. is excluded, when for example T is inferred and is guaranteed to be precise: const f: <T>(t: T) => void = (t) => { Object.keys(t).forEach(k => t[k]) }. I have lots of places like that in my code, where I really want Object.keys() to return (keyof T).

    – artem
    3 hours ago













  • As arthem also points out, the confusion comes from the fact that 9 out of 10 times you will end up in some way using a type assertion to keyof T to do anything useful with the result of keys. You might argue it is better to be explicit about it so you are more aware of the risk you are taking, but probably 9/10 devs will just add the type assertion and not be aware of the issues you highlight ..

    – Titian Cernicova-Dragomir
    1 hour ago
















12












12








12







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.






share|improve this answer















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.







share|improve this answer














share|improve this answer



share|improve this answer








edited 2 hours ago









Tholle

38.1k54264




38.1k54264










answered 3 hours ago









Ryan CavanaughRyan Cavanaugh

100k27171182




100k27171182








  • 1





    However, there is common case when point 3. is excluded, when for example T is inferred and is guaranteed to be precise: const f: <T>(t: T) => void = (t) => { Object.keys(t).forEach(k => t[k]) }. I have lots of places like that in my code, where I really want Object.keys() to return (keyof T).

    – artem
    3 hours ago













  • As arthem also points out, the confusion comes from the fact that 9 out of 10 times you will end up in some way using a type assertion to keyof T to do anything useful with the result of keys. You might argue it is better to be explicit about it so you are more aware of the risk you are taking, but probably 9/10 devs will just add the type assertion and not be aware of the issues you highlight ..

    – Titian Cernicova-Dragomir
    1 hour ago
















  • 1





    However, there is common case when point 3. is excluded, when for example T is inferred and is guaranteed to be precise: const f: <T>(t: T) => void = (t) => { Object.keys(t).forEach(k => t[k]) }. I have lots of places like that in my code, where I really want Object.keys() to return (keyof T).

    – artem
    3 hours ago













  • As arthem also points out, the confusion comes from the fact that 9 out of 10 times you will end up in some way using a type assertion to keyof T to do anything useful with the result of keys. You might argue it is better to be explicit about it so you are more aware of the risk you are taking, but probably 9/10 devs will just add the type assertion and not be aware of the issues you highlight ..

    – Titian Cernicova-Dragomir
    1 hour ago










1




1





However, there is common case when point 3. is excluded, when for example T is inferred and is guaranteed to be precise: const f: <T>(t: T) => void = (t) => { Object.keys(t).forEach(k => t[k]) }. I have lots of places like that in my code, where I really want Object.keys() to return (keyof T).

– artem
3 hours ago







However, there is common case when point 3. is excluded, when for example T is inferred and is guaranteed to be precise: const f: <T>(t: T) => void = (t) => { Object.keys(t).forEach(k => t[k]) }. I have lots of places like that in my code, where I really want Object.keys() to return (keyof T).

– artem
3 hours ago















As arthem also points out, the confusion comes from the fact that 9 out of 10 times you will end up in some way using a type assertion to keyof T to do anything useful with the result of keys. You might argue it is better to be explicit about it so you are more aware of the risk you are taking, but probably 9/10 devs will just add the type assertion and not be aware of the issues you highlight ..

– Titian Cernicova-Dragomir
1 hour ago







As arthem also points out, the confusion comes from the fact that 9 out of 10 times you will end up in some way using a type assertion to keyof T to do anything useful with the result of keys. You might argue it is better to be explicit about it so you are more aware of the risk you are taking, but probably 9/10 devs will just add the type assertion and not be aware of the issues you highlight ..

– Titian Cernicova-Dragomir
1 hour ago






















draft saved

draft discarded




















































Thanks for contributing an answer to Stack Overflow!


  • Please be sure to answer the question. Provide details and share your research!

But avoid



  • Asking for help, clarification, or responding to other answers.

  • Making statements based on opinion; back them up with references or personal experience.


To learn more, see our tips on writing great answers.




draft saved


draft discarded














StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f55012174%2fwhy-doesnt-object-keys-return-a-keyof-type-in-typescript%23new-answer', 'question_page');
}
);

Post as a guest















Required, but never shown





















































Required, but never shown














Required, but never shown












Required, but never shown







Required, but never shown

































Required, but never shown














Required, but never shown












Required, but never shown







Required, but never shown







Popular posts from this blog

Lallio

Unable to find Lightning Node

Futebolista