microsoft/TypeScript

Design Meeting Notes, 10/28/2016

mhegazy opened this issue · 1 comments

keyof T and T[K]

Note about terminology

  • keyof is a type operator. Note, name change from #10425 to keyof; 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 call number and not numbers,
  • Henceforth, formal names of these type operators:
    • keyof T => Index Type Query
    • T[K] => Indexed Access Type

Details

keyof T

  • known properties and index signatures (number | string for string indexers and number 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 and protected
    A piece of trivia, accessing private and protected properties are allowed for index access today

    class 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

T[K]

  • only index with a type K parameter iff it is constraint to the keyof 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 expression t[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 and readwrite keyof T.

Absolutely awesome. This adds a whole new dimension of expressiveness to the language and does so in a way that is just brilliantly JavaScript.

keyof {}[] => "toString" | "pop" |...| number

is just beautiful.