Closed
Description
keyof T
and T[K]
Note about terminology
keyof
is a type operator. Note, name change from Keysof type operator #10425 tokeyof
; since the type name represents one value and not the whole value domain. this is inline with the other names of types e.g. we callnumber
and notnumbers
,- Henceforth, formal names of these type operators:
keyof T
=> Index Type QueryT[K]
=> Indexed Access Type
Details
keyof T
-
known properties and index signatures (
number | string
for string indexers andnumber
for numeric indexers) -
Examples:
keyof { a: number, b:string}
=>"a" | "b"
keyof { [x: string] : number }
=>string | number
keyof { [x: number] : number }
=>number
keyof {}[]
=>"toString" | "pop" |...| number
keyof { a: number, b:string } | { a: number, c:string}
=>"a"
keyof { a: number, b:string } & { a: number, c:string}
=>"a" | "b" | "c"
-
What about symbols?
- symbols not supported yet. we need to make symbols literal types first.
-
private
andprotected
A piece of trivia, accessing private and protected properties are allowed for index access todayclass C { private x; } let c: C; c.x; // not allowed c["x"]; // is allowed today
- should this apply to the new type operator:
ts type T = C["x"]; // OK or not?
- for
nameof
in C# does not do this either- it exists at run time
- against
- we should not carry past misdeeds in new features
- it exposes implementation details
- not valid for declaration emit, since we strip out the type
- Decision
keyof T
is public-only known properties, private and protected properties are not included
- should this apply to the new type operator:
T[K]
- only index with a type
K
parameter iff it is constraint to thekeyof T
- this grantees that the output is type of a property
- can be used with literal types
class Thing {
name: string;
width: number;
}
type q1 = Thing["name"]; // string
type q2 = Thing["name" | "width"]; //string | number
type q3= Thing["name" | "foo"]; // Error "foo" is not a property on Thing
Putting them together
function getProperty<T, K extends keyof T>(obj: T, key:K) {
return obj[key]; // T[K]
}
function setProperty<T, K extends keyof T>(obj: T, key:K, value: T[K]) {
obj[K] = value;
}
var thing: Thing;
var thingList: Thing[];
getPropeprty(thing, "foo") // Error
getPropeprty(thing, "name") // string
getPropeprty(thingList, 1) // Thing
getPropeprty(thingList, 1 + 2) // Thing
setPropoerty(thing, "name", "my name"); // OK
setPropoerty(thing, "name", 3); // Error
Index access unification
- The way we do
T[K]
for a type should be the same as that for an expressiont[k]
. - so
thing[condition ? "name" : "width"]; // should be string | number
Issues
Variance
Consider this:
setPropoerty(thing, condition? "name" : "width", "string");
- this is allowed today, but it is unsafe...
Keyof T
should be a union for types of all properties for read, but an intersection for write. - This is the same behavior we have today for indexers anyways:
interface I {
a: number;
b: string;
[x: string] : number | string;
}
const k = condition? "a" : "b";
x[k] // number | string;
x[k] = "bar"; // allowed but not safe
- We are getting close to the "safe variance" glass ceiling
- solving these all would need to add variance in the type system
- a readonly, writeonly, and readwrite types
- this applies to class properties, parameters, everything
- this would be a large braking change, and have to be done holistically
- the safer it would be the tighter it would be for users to use
Where we can land:
- For expressions, it would be an error i.e.
thing[condition ? "name" : "width"] = "bar"; /// Error, we know unsafe
- but not for generic type argument
setProperty(thing, condition ? "name" : "width", "bar"); // OK
Note about readonly
:
- you can write a readonly propoerty using
setPropoerty
. - there is not much we can do here. the time where this would be observed is at instantiation time, which is too late at that point to report errors.
- and again it goes back to variance, you need
readonly keyof T
andreadwrite keyof T
.