Skip to content

Conversation

@ahejlsberg
Copy link
Member

@ahejlsbergahejlsberg 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).

typeBox<T>={value: T;}typeBoxified<T>={[PinkeyofT]: Box<T[P]>;}functionbox<T>(x: T): Box<T>{return{value: x};}functionunbox<T>(x: Box<T>): T{returnx.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.functionboxify<T>(obj: T): Boxified<T>{letresult={}asBoxified<T>;for(letkinobj){result[k]=box(obj[k]);}returnresult;}// 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).functionunboxify<T>(obj: Boxified<T>): T{letresult={}asT;for(letkinobj){result[k]=unbox(obj[k]);}returnresult;}// Type inference for calls to assignBoxified is a combination of the two cases// above.functionassignBoxified<T>(obj: Boxified<T>,values: T){for(letkinvalues){obj[k].value=values[k];}}functionf1(){letv={a: 42,b: "hello",c: true};// Infers type{a: Box<number>, b: Box<string>, c: Box<boolean>} for b.letb=boxify(v);letx: number=b.a.value;}functionf2(){letb={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.letv=unboxify(b);letx: number=v.a;}functionf3(){letb={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});}functionf4(){letb={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.functionmakeRecord<T,Kextendsstring>(obj: {[PinK]: T}){returnobj;}functionf5(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.letb=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. letv=unboxify(b);letx: 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.functionmakeDictionary<T>(obj: {[x: string]: T}){returnobj;}functionf6(s: string){// The inferred type for b is{[x: string]: Box<number> | Box<string > |// Box<boolean> }.letb=makeDictionary({a: box(42),b: box("hello"),c: box(true)});// The inferred type for v is{[x: string]: number | string | boolean }.letv=unboxify(b);letx: 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
MemberAuthor

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

This was referenced Nov 28, 2016
@spion
Copy link

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:

typeBox<T>={value: T}typeLoc={loc: number}functionaddLocations<T,BextendsBlock<Box<T>>,Oextends{[KinkeyofB]: B[K]&Loc}>(b: B): O{constout: O={}for(constkeyinb){out[key]={value: b[key].value,loc: 0}}returnout}constbs={age: {value: 5}}constbts=addLocations(bs)

@ahejlsberg
Copy link
MemberAuthor

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):

typeBox<T>={value: T}typeLoc={loc: number}typeBoxified<T>={[PinkeyofT]: Box<T[P]>}typeBoxLocified<T>={[PinkeyofT]: Box<T[P]>&Loc}functionaddLocations<T>(b: Boxified<T>): BoxLocified<T>{constresult={}asBoxLocified<T>;for(constkeyinb){result[key]={value: b[key].value,loc: 0};}returnresult;}

@ahejlsberg
Copy link
MemberAuthor

@spion Much appreciate the kind words!

@ahejlsbergahejlsberg merged commit 5dd4c9e into masterNov 28, 2016
@ahejlsbergahejlsberg 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.

@ahejlsbergahejlsberg changed the title Type inference for isomorphic mapped typesType inference for homomorphic mapped typesNov 30, 2016
@microsoftmicrosoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for freeto 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.

7 participants

@ahejlsberg@DanielRosenwasser@spion@stevekane@mhegazy@msftclas