Skip to content

Conversation

@ahejlsberg
Copy link
Member

@ahejlsbergahejlsberg commented Oct 28, 2016

This PR adds new typing constructs that enable static validation of code involving dynamic property names and properties selected by such dynamic names. For example, in JavaScript it is fairly common to have APIs that expect property names as parameters, but so far it hasn't been possible to express the type relationships that occur in those APIs. The PR also improves error messages related to missing properties and/or index signatures.

The PR is inspired by #1295 and #10425 and the discussions in those issues. The PR implements two new type constructs:

  • Index type queries of the form keyof T, where T is some type.
  • Indexed access types of the form T[K], where T is some type and K is a type that is assignable to keyof T (or assignable to number if T contains a numeric index signature).

An index type query keyof T yields the type of permitted property names for T. A keyof T type is considered a subtype of string. When T is not a type parameter, keyof T is resolved as follows:

  • If T has no apparent string index signature, keyof T is a type of the form "p1" | "p2" | ... | "pX", where the string literals represent the names of the public properties of T. If T has no public properties, keyof T is the type never.
  • If T has an apparent string index signature, keyof T is the type string.

Note that keyof T ignores numeric index signatures. To properly account for those we would need a numericstring type to represent strings contain numeric representations.

An indexed access type T[K] requires K to be a type that is assignable to keyof T (or assignable to number if T contains a numeric index signature) and yields the type of the property or properties in T selected by K. T[K] permits K to be a type parameter, in which case K must be constrained to a type that is assignable to keyof T. Otherwise, when K is not a type parameter, T[K] is resolved as follows:

  • If K is a union type K1 | K2 | ... | Kn, T[K] is equivalent to T[K1] | T[K2] | ... | T[Kn].
  • If K is a string literal type, numeric literal type, or enum literal type, and T contains a public property with the name given by that literal type, T[K] is the type of that property.
  • If K is a type assignable to number and T contains a numeric index signature, T[K] is the type of that numeric index signature.
  • If K is a type assignable to string and T contains a string index signature, T[K] is the type of that string index signature.

Some examples:

interfaceThing{name: string;width: number;height: number;inStock: boolean;}typeK1=keyofThing;// "name" | "width" | "height" | "inStock"typeK2=keyofThing[];// "length" | "push" | "pop" | "concat" | ...typeK3=keyof{[x: string]: Thing};// stringtypeP1=Thing["name"];// stringtypeP2=Thing["width"|"height"];// numbertypeP3=Thing["name"|"inStock"];// string | booleantypeP4=string["charAt"];// (pos: number) => stringtypeP5=string[]["push"];// (...items: string[]) => numbertypeP6=string[][0];// string

An indexed access type T[K] permits K to be a type parameter that is constrained to keyof T. This makes it possible to do parametric abstraction over property access:

functiongetProperty<T,KextendskeyofT>(obj: T,key: K){returnobj[key];// Inferred type is T[K]}functionsetProperty<T,KextendskeyofT>(obj: T,key: K,value: T[K]){obj[key]=value;}functionf1(thing: Thing,propName: "name"|"width"){letname=getProperty(thing,"name");// Ok, type stringletsize=getProperty(thing,"size");// Error, no property named "size"setProperty(thing,"width",42);// OksetProperty(thing,"color","blue");// Error, no property named "color"letnameOrWidth=getProperty(thing,propName);// Ok, type string | number}functionf2(tuple: [string,number,Thing]){letlength=getProperty(tuple,"length");// Ok, type numberconstTWO="2";lett0=getProperty(tuple,"0");// Ok, type stringlett1=getProperty(tuple,"1");// Ok, type numberlett2=getProperty(tuple,TWO);// Ok, type Thing}classComponent<PropType>{props: PropType;getProperty<KextendskeyofPropType>(key: K){returnthis.props[key];}setProperty<KextendskeyofPropType>(key: K,value: PropType[K]){this.props[key]=value;}}functionf3(component: Component<Thing>){letwidth=component.getProperty("width");// Ok, type numbercomponent.setProperty("name","test");// Ok}functionpluck<T,KextendskeyofT>(array: T[],key: K){returnarray.map(x=>x[key]);}functionf4(things: Thing[]){letnames=pluck(things,"name");// string[]letwidths=pluck(things,"width");// number[]}

Note: This description has been edited to reflect the changes implemented in #12425.

@weswigham
Copy link
Member

weswigham commented Oct 29, 2016

@ahejlsberg Does this also adjust the logic for handling actual index expressions? For example,

interfaceFoo{a: 0b: 1}varx: "a"|"b";vary: Foo;varz=y[x];// Is this Foo["a" | "b"] or 0 | 1 (rather than relying on a nonexistant index signature for the type or being any)?

Or is actual indexing unchanged? IMO, it would make the most sense if the typespace indexing operator and the valuespace one affected type flow in the same way.

This was the last half-feature/concern I had identified in the original issue.

@ahejlsberg
Copy link
MemberAuthor

ahejlsberg commented Oct 29, 2016

@weswigham The operation y[x] will produce type 0 | 1 once I do the remaining work to unify indexing in the expression world with indexing in the type world.

EDIT: The remaining unification is done and y[x] now produces type 0 | 1.

BTW, there is an interesting issue around reading vs. writing when the index type is not a unit type. Technically, it is only safe to union together the resulting values when you're reading. For writing, the only safe construct would be an intersection, but that is rarely useful. So, we're planning to make writing an error when the index type is not a unit type:

interfaceFoo{a: 0,b: 1}functionf1(foo: Foo,x: "a"|"b"){constc=foo[x];// Ok, type 0 | 1foo[x]=c;// Error, absent index signatures an index expresion must be of a unit type}

However, in the parametric case we make no distinction between keyof T types used for reading vs. writing, so you can "cheat":

functionsetProperty<T,KextendskeyofT>(obj: T,key: K,value: T[K]){obj[key]=value;}functionf2(foo: Foo,x: "a"|"b"){constc=foo[x];// Ok, type 0 | 1setProperty(foo,x,c);// Ok}

It actually seems fairly reasonable to allow this. Really, there are three distinct possibilities:

  • The operation is known to never be safe. We always error in such cases.
  • The operation could be safe provided index and value match appropriately. We permit that only in generic cases.
  • The operation is known to always be safe. We never error in such cases.

The alternative is to have distinct safe for reading, safe for writing, and safe for reading and writing versions of keyof T. That seems like overkill, or at best an orthogonal issue we should consider if we ever decide to go all in on variance annotations.

@tinganhotinganho mentioned this pull request Oct 31, 2016
@ahejlsberg
Copy link
MemberAuthor

ahejlsberg commented Oct 31, 2016

Latest set of commits unify type checking of indexed access expressions and indexed access types. Also, improved error messages for a number of errors related to property/element access and constant/read-only checking.

# Conflicts: # src/compiler/diagnosticMessages.json
/* @internal */
resolvedIndexType: IndexType;
/* @internal */
resolvedIndexedAccessTypes: IndexedAccessType[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why array instead of map? the array is going to be sparse if indexed with type id.

Copy link
MemberAuthor

@ahejlsbergahejlsbergNov 1, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it will be sparse, in the same way that our nodeLinks and symbolLinks arrays are sparse. I believe this performs better than a map.

return indexedAccessTypes[objectType.id] || (indexedAccessTypes[objectType.id] = createIndexedAccessType(objectType, indexType));
}

function getPropertyTypeForIndexType(objectType: Type, indexType: Type, accessNode?: ElementAccessExpression | IndexedAccessTypeNode){
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should not this be getPropertyTypeForIndexedType or getPropertyTypeForIndexAccess since it operates on the indexed type rather than the index type.

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, it operates both on the indexed type and the index type. So, technically it should be getPropertyTypeForIndexedTypeIndexedByIndexType. 😃 Honestly, not sure what would be a good name.

"code": 7016
},
"Index signature of object type implicitly has an 'any' type.":{
"Element implicitly has an 'any' type because type '{0}' has no index signature.":{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉 this is much better now!

error(indexNode, Diagnostics.Property_0_does_not_exist_on_type_1, (<LiteralType>indexType).text, typeToString(objectType));
}
else if (indexType.flags & (TypeFlags.String | TypeFlags.Number)){
error(accessNode, Diagnostics.Type_0_has_no_matching_index_signature_for_type_1, typeToString(objectType), typeToString(indexType));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i do not see any tests for this scenario.

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I still haven't added tests.

Copy link
Contributor

@mhegazymhegazy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not see any tests for keyof and/or T[K]. also please include some declaration emit tests.

@HerringtonDarkholme
Copy link
Contributor

I wonder whether index access type works with constrained generic parameter. For example, only the last alias of the following three is resolved to number.

typeA<Textends{[Kin"test"]: any}>=T["test"]typeB<Textends{test: any}>=T["test"]typeC<Textends{test: any},KextendskeyofT>=T[K]typeA1=A<{test: number}>typeB1=B<{test: number}>typeC1=C<{test: number},"test">

Is this intended?

@ahejlsberg
Copy link
MemberAuthor

@HerringtonDarkholme In an indexed access T["test"], where T is a type parameter, we eagerly resolve the type based on the constraint of T. So if T has the constraint {test: any }, we'll eagerly resolve to type any. We only defer the operation when the index type is also a type parameter. Deferring T["test"] would have a lot of downstream effects on existing code because it changes every access to a property of this within a class to be a deferred generic type. It might be possible to do it at some point, but it is not a short-term option.

@fula2013
Copy link

@ahejlsberg what about support for regex check type? like this:
type SomeTel = match of /^\d{4}-\d{6}$/; var tel: SomeTel = '0768-888888'; //is ok

@CodySchaaf
Copy link

Is there no way to make private members accessible? Would be nice if you could do something similar to the readonly transformation.

My Specific example is when transforming the current class to the IOnChangesObject in angular.

type IOnChangesObject<T> ={[P in keyof T]: ng.IChangesObject<T[P]>} $onChanges(changesObj: IOnChangesObject<Ctrl>){//both private and public attributes should work on changesObj } 

@nahakyuu
Copy link

typeFoo<T>=T&{Test: ()=>void;}typeFoo1<T>=keyofFoo<T>;typeFoo2=Foo1<{prop: string}>;

type Foo2 = "Test"

@nevir
Copy link

Should this also work for object literals? I would have expected the following to also work:

constthing={a: 'foo',b: 'bar',};// Expected: "a" | "b"// Actual: Cannot find name 'thing'typethingKey=keyofthing;

@gcnew
Copy link
Contributor

gcnew commented Dec 20, 2016

@nevir Use typeof to get the type of thing

typethingKey=keyoftypeofthing;

@nevir
Copy link

Ah! Thanks!

@Artazor
Copy link
Contributor

Artazor commented Mar 14, 2017

just want to share it here:
(Advanced usage of mapped types and static types for dynamically named properties)

////////////////////////////////// USAGE /////////////////////////////////varsequelize=newSequelize();varUsers=sequelize.define("users",{firstName: STRING,lastName: CHAR(123),age: INTEGER,visits: {type: INTEGER,length: 4}});varu=Users.create({age: 22});// only valid fields/types allowed u.firstName="John";// types are checked at compile timeu.lastName="Doe"u.visits=123;///////////////////////////////// DECLARATIONS /////////////////////////////interfaceABSTRACT_STATIC<T>{prototype: ABSTRACT<string,T>;}interfaceABSTRACT_BASE<T>{stringify(value: T,options): string;validate(value: T);}interfaceABSTRACT<Key,T>extendsABSTRACT_BASE<T>{key: Key;dialectTypes: string;toSql: string;warn(link: string,text);}interfaceSTRING_OPTIONS{length?: number,binary?: boolean}interfaceSTRING<Key>extendsABSTRACT<Key,string>{readonlyBINARY: this }interfaceSTRING_STATIC<Key,TextendsSTRING<Key>>extendsABSTRACT_STATIC<string>{new(length?: number,binary?: boolean): T;new(options: STRING_OPTIONS): T;(length?: number,binary?: boolean): T;(options: STRING_OPTIONS): T;}declareconstSTRING: STRING_STATIC<"STRING",STRING<"STRING">>;interfaceCHARextendsSTRING<"CHAR">{}interfaceCHAR_STATICextendsSTRING_STATIC<"CHAR",CHAR>{}declareconstCHAR: CHAR_STATIC;interfaceNUMBER_OPTIONS{length?: number,zerofill?: boolean,decimals?: number,precision?: number,scale?: number,unsigned?: boolean}interfaceNUMBER<Key>extendsABSTRACT<Key,number>{readonlyUNSIGNED: this readonlyZEROFILL: this }interfaceNUMBER_STATIC<Key,TextendsNUMBER<Key>>extendsABSTRACT_STATIC<number>{new(options: NUMBER_OPTIONS): T;(options: NUMBER_OPTIONS): T;}declareconstNUMBER: NUMBER_STATIC<"NUMBER",NUMBER<"NUMBER">>;interfaceINTEGERextendsNUMBER<"INTEGER">{}interfaceINTEGER_STATICextendsNUMBER_STATIC<"INTEGER",INTEGER>{new(): INTEGER;(): INTEGER;new(length: number): INTEGER;(length: number): INTEGER;}declareconstINTEGER: INTEGER_STATIC;interfaceOPTIONS<T>{type: ABSTRACT_STATIC<T>}interfaceSTRING_OPTIONS_TYPEextendsOPTIONS<string>,STRING_OPTIONS{}interfaceNUMBER_OPTIONS_TYPEextendsOPTIONS<number>,NUMBER_OPTIONS{}typeFIELD<T>=ABSTRACT_STATIC<T>|OPTIONS<T>&(STRING_OPTIONS_TYPE|NUMBER_OPTIONS_TYPE)|ABSTRACT_BASE<T>typeINIT<T>={[PinkeyofT]?:T[P]}declareclassFactory<T>{create(initial?: INIT<T>): T}declareclassSequelize{define<T>(tableName: string,fields: {[PinkeyofT]: FIELD<T[P]>}):Factory<T>}

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.

20 participants

@ahejlsberg@weswigham@ThomasMichon@mhegazy@jods4@dasa@RobertoMalatesta@pelotom@zpdDG4gta8XKpMCd@wclr@PyroVortex@mohsen1@pleerock@HerringtonDarkholme@fula2013@CodySchaaf@nahakyuu@nevir@gcnew@Artazor