Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type inference for homomorphic mapped types #12528

Merged
merged 6 commits into from
Nov 28, 2016
Merged

Conversation

ahejlsberg
Copy link
Member

@ahejlsberg ahejlsberg commented Nov 27, 2016

This PR introduces deeper type inference for homomorphic (structure preserving) mapped types. In particular, with this PR we now have the ability to infer unmapped forms of mapped types: When inferring from a type S to a homomorphic mapped type { [P in keyof T]: X }, we attempt to infer a suitable type for T that satisfies the mapping expressed by X. We construct an object type with the same set of properties as S, where the type of each property is computed by inferring from the source property type to X for a synthetic type parameter T[P] (i.e. we treat the type T[P] as the type parameter we're inferring for).

type Box<T> = {
    value: T;
}

type Boxified<T> = {
    [P in keyof T]: Box<T[P]>;
}

function box<T>(x: T): Box<T> {
    return { value: x };
}

function unbox<T>(x: Box<T>): T {
    return x.value;
}

// Type inference is trivial for calls to boxify because we just infer the input
// type for T. The Boxified<T> return type then wraps a box around each property.
function boxify<T>(obj: T): Boxified<T> {
    let result = {} as Boxified<T>;
    for (let k in obj) {
        result[k] = box(obj[k]);
    }
    return result;
}

// Type inference in calls to unboxify is far less trivial because we need to
// reverse the mapping performed by Boxify<T>. When inferring from a type S to a
// homomorphic mapped type { [P in keyof T]: X }, we attempt to infer a suitable
// type for T that satisfies the mapping expressed by X. We construct an object
// type with the same set of properties as S, where the type of each property is
// computed by inferring from the source property type to X for a synthetic type
// parameter T[P] (i.e. we treat the type T[P] as the type parameter we're
// inferring for).
function unboxify<T>(obj: Boxified<T>): T {
    let result = {} as T;
    for (let k in obj) {
        result[k] = unbox(obj[k]);
    }
    return result;
}

// Type inference for calls to assignBoxified is a combination of the two cases
// above.
function assignBoxified<T>(obj: Boxified<T>, values: T) {
    for (let k in values) {
        obj[k].value = values[k];
    }
}

function f1() {
    let v = {
        a: 42,
        b: "hello",
        c: true
    };
    // Infers type { a: Box<number>, b: Box<string>, c: Box<boolean> } for b.
    let b = boxify(v);
    let x: number = b.a.value;
}

function f2() {
    let b = {
        a: box(42),
        b: box("hello"),
        c: box(true)
    };
    // Infers type { a: number, b: number, c: number } for v. Effectively, we
    // reversely infer the type that, when boxified, becomes the type of b.
    let v = unboxify(b);
    let x: number = v.a;
}

function f3() {
    let b = {
        a: box(42),
        b: box("hello"),
        c: box(true)
    };
    // We make two inferences for T here, { a: number, b: string, c: boolean }
    // and { c: boolean }. This reduces to { c: boolean }.
    assignBoxified(b, { c: false });
}

function f4() {
    let b = {
        a: box(42),
        b: box("hello"),
        c: box(true)
    };
    b = boxify(unboxify(b));
    b = unboxify(boxify(b));
}

// When inferring from some source type S to a mapped type { [P in K]: X }, we infer
// from 'keyof S' to K and infer from a union of each property type in S to X. Thus,
// we end up producing a type that has the same set of properties as S with a
// uniform type for all of the properties.
function makeRecord<T, K extends string>(obj: { [P in K]: T }) {
    return obj;
}

function f5(s: string) {
    // The inferred type for b is { a: X, b: X, c: X}, where X is Box<number> |
    // Box<string> | Box<boolean>. In effect we union the type of the properties in
    // the input to produce a uniform type in the output.
    let b = makeRecord({
        a: box(42),
        b: box("hello"),
        c: box(true)
    });
    // The inferred type for v is { a: Y, b: Y, c: Y }, where Y is number |
    // string | boolean. 
    let v = unboxify(b);
    let x: string | number | boolean = v.a;
}

// The type { [x: string]: T } can also be written { [P in string]: T }. When
// inferring from a type S to { [P in string]: X }, we infer from a union of each
// property type in S to X.
function makeDictionary<T>(obj: { [x: string]: T }) {
    return obj;
}

function f6(s: string) {
    // The inferred type for b is { [x: string]: Box<number> | Box<string > |
    // Box<boolean> }.
    let b = makeDictionary({
        a: box(42),
        b: box("hello"),
        c: box(true)
    });
    // The inferred type for v is { [x: string]: number | string | boolean }.
    let v = unboxify(b);
    let x: string | number | boolean = v[s];
}

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Nov 27, 2016

@ahejlsberg That is a lot of code - could you explain what exactly the before/after is here? What wasn't working before? What works with this change?

@ahejlsberg
Copy link
Member Author

@DanielRosenwasser I've added a bunch of comments to the examples.

This was referenced Nov 28, 2016
@spion
Copy link

spion commented Nov 28, 2016

This is just incredible. I thought about just using the reaction button, but I feel like thats not enough to express how awesome I believe this is. I previously thought that some dynamic aspects of JS were unbeatable by any type system, but now I'm having serious doubts. The set of things that cannot be expressed with TypeScript's type system is shrinking very quickly!

Thanks @ahejlsberg - truly amazing work on mapped types.

@stevekane
Copy link

stevekane commented Nov 28, 2016

@ahejlsberg is this PR available already on next? I'm still having quite a hard time implementing these kinds of APIs.

The code below should ADD a location to every value in the object it is passed:

type Box<T> = { value: T }
type Loc = { loc: number }

function addLocations
<T, 
 B extends Block<Box<T>>, 
 O extends { [ K in keyof B ]: B[K] & Loc }> 
( b: B ): O {
  const out: O = {}

  for ( const key in b ) {
    out[key] = { value: b[key].value, loc: 0 }
  }
  return out
}

const bs = { age: { value: 5 } }
const bts = addLocations(bs)

@ahejlsberg
Copy link
Member Author

ahejlsberg commented Nov 28, 2016

@stevekane Just merged the PR, it will be in tonight's nightly.

The problem in your example is that you're using constrained type parameters instead of actual types. As a rule of thumb, constraints are checked after type inference, but do not actually participate in type inference. For that reason, the compiler doesn't "see" the relationships you're trying to express. You should instead have the minimal number of type parameters and express the relationships in the actual parameter types. For example, the following works (when you use a branch with this PR):

type Box<T> = { value: T }
type Loc = { loc: number }

type Boxified<T> = {
    [P in keyof T]: Box<T[P]>
}

type BoxLocified<T> = {
    [P in keyof T]: Box<T[P]> & Loc
}

function addLocations<T>(b: Boxified<T>): BoxLocified<T> {
    const result = {} as BoxLocified<T>;
    for (const key in b) {
        result[key] = { value: b[key].value, loc: 0 };
    }
    return result;
}

@ahejlsberg
Copy link
Member Author

@spion Much appreciate the kind words!

@ahejlsberg ahejlsberg merged commit 5dd4c9e into master Nov 28, 2016
@ahejlsberg ahejlsberg deleted the mappedTypeInference branch November 28, 2016 21:30
@stevekane
Copy link

Beautiful @ahejlsberg. Thanks for taking the time to provide that example and the valuable explanation. I am now fully up-and-running w/ this type-safe webgl library and will update the issues I have opened with comments saying as much. Very fine work here. Your speed and clarity of implementation is impressive.

@ahejlsberg ahejlsberg changed the title Type inference for isomorphic mapped types Type inference for homomorphic mapped types Nov 30, 2016
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants