-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Static types for dynamically named properties #11929
Conversation
@ahejlsberg Does this also adjust the logic for handling actual index expressions? For example, interface Foo {
a: 0
b: 1
}
var x: "a" | "b";
var y: Foo;
var z = 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. |
@weswigham The operation EDIT: The remaining unification is done and 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: interface Foo { a: 0, b: 1 }
function f1(foo: Foo, x: "a" | "b") {
const c = foo[x]; // Ok, type 0 | 1
foo[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 function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]) {
obj[key] = value;
}
function f2(foo: Foo, x: "a" | "b") {
const c = foo[x]; // Ok, type 0 | 1
setProperty(foo, x, c); // Ok
} It actually seems fairly reasonable to allow this. Really, there are three distinct possibilities:
The alternative is to have distinct safe for reading, safe for writing, and safe for reading and writing versions of |
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
@@ -2860,9 +2873,22 @@ namespace ts { | |||
/* @internal */ | |||
resolvedApparentType: Type; | |||
/* @internal */ | |||
resolvedIndexType: IndexType; | |||
/* @internal */ | |||
resolvedIndexedAccessTypes: IndexedAccessType[]; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
@@ -2909,7 +2921,7 @@ | |||
"category": "Error", | |||
"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.": { |
There was a problem hiding this comment.
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)); |
There was a problem hiding this 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 this scenario.
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
I wonder whether index access type works with constrained generic parameter. For example, only the last alias of the following three is resolved to type A<T extends {[K in "test"]: any}> = T["test"]
type B<T extends {test: any}> = T["test"]
type C<T extends {test: any}, K extends keyof T> = T[K]
type A1 = A<{test: number}>
type B1 = B<{test: number}>
type C1 = C<{test: number}, "test"> Is this intended? |
@HerringtonDarkholme In an indexed access |
@ahejlsberg what about support for regex check type? like this: |
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 Foo<T> = T & {
Test: () => void;
}
type Foo1<T> = keyof Foo<T>;
type Foo2 = Foo1<{ prop: string }>; type Foo2 = "Test" |
Should this also work for object literals? I would have expected the following to also work: const thing = {
a: 'foo',
b: 'bar',
};
// Expected: "a" | "b"
// Actual: Cannot find name 'thing'
type thingKey = keyof thing; |
@nevir Use type thingKey = keyof typeof thing; |
Ah! Thanks! |
just want to share it here: ////////////////////////////////// USAGE /////////////////////////////////
var sequelize = new Sequelize();
var Users = sequelize.define("users", {
firstName: STRING,
lastName: CHAR(123),
age: INTEGER,
visits: { type: INTEGER, length: 4}
});
var u = Users.create({age: 22}); // only valid fields/types allowed
u.firstName = "John"; // types are checked at compile time
u.lastName = "Doe"
u.visits = 123;
///////////////////////////////// DECLARATIONS /////////////////////////////
interface ABSTRACT_STATIC<T> {
prototype: ABSTRACT<string, T>;
}
interface ABSTRACT_BASE<T> {
stringify(value: T, options): string;
validate(value: T);
}
interface ABSTRACT<Key, T> extends ABSTRACT_BASE<T> {
key: Key;
dialectTypes: string;
toSql: string;
warn(link: string, text);
}
interface STRING_OPTIONS { length?: number, binary?: boolean }
interface STRING<Key> extends ABSTRACT<Key, string> {
readonly BINARY: this
}
interface STRING_STATIC<Key, T extends STRING<Key>> extends ABSTRACT_STATIC<string> {
new(length?: number, binary?: boolean): T;
new(options: STRING_OPTIONS): T;
(length?: number, binary?: boolean): T;
(options: STRING_OPTIONS): T;
}
declare const STRING: STRING_STATIC<"STRING", STRING<"STRING">>;
interface CHAR extends STRING<"CHAR"> {}
interface CHAR_STATIC extends STRING_STATIC<"CHAR", CHAR> {}
declare const CHAR: CHAR_STATIC;
interface NUMBER_OPTIONS {
length?: number,
zerofill?: boolean,
decimals?: number,
precision?: number,
scale?: number,
unsigned?: boolean
}
interface NUMBER<Key> extends ABSTRACT<Key, number> {
readonly UNSIGNED: this
readonly ZEROFILL: this
}
interface NUMBER_STATIC<Key, T extends NUMBER<Key>> extends ABSTRACT_STATIC<number> {
new(options: NUMBER_OPTIONS): T;
(options: NUMBER_OPTIONS): T;
}
declare const NUMBER: NUMBER_STATIC<"NUMBER", NUMBER<"NUMBER">>;
interface INTEGER extends NUMBER<"INTEGER"> {}
interface INTEGER_STATIC extends NUMBER_STATIC<"INTEGER", INTEGER> {
new(): INTEGER;
(): INTEGER;
new(length: number): INTEGER;
(length: number): INTEGER;
}
declare const INTEGER: INTEGER_STATIC;
interface OPTIONS<T> { type: ABSTRACT_STATIC<T> }
interface STRING_OPTIONS_TYPE extends OPTIONS<string>, STRING_OPTIONS { }
interface NUMBER_OPTIONS_TYPE extends OPTIONS<number>, NUMBER_OPTIONS { }
type FIELD<T> = ABSTRACT_STATIC<T>
| OPTIONS<T> & (
STRING_OPTIONS_TYPE |
NUMBER_OPTIONS_TYPE
)
| ABSTRACT_BASE<T>
type INIT<T> = {[P in keyof T]?:T[P]}
declare class Factory<T> {
create(initial?: INIT<T>): T
}
declare class Sequelize {
define<T>(tableName: string, fields: {[P in keyof T]: FIELD<T[P]>}):Factory<T>
} |
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:
keyof T
, whereT
is some type.T[K]
, whereT
is some type andK
is a type that is assignable tokeyof T
(or assignable tonumber
ifT
contains a numeric index signature).An index type query
keyof T
yields the type of permitted property names forT
. Akeyof T
type is considered a subtype ofstring
. WhenT
is not a type parameter,keyof T
is resolved as follows: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 ofT
. IfT
has no public properties,keyof T
is the typenever
.T
has an apparent string index signature,keyof T
is the typestring
.Note that
keyof T
ignores numeric index signatures. To properly account for those we would need anumericstring
type to represent strings contain numeric representations.An indexed access type
T[K]
requiresK
to be a type that is assignable tokeyof T
(or assignable to number if T contains a numeric index signature) and yields the type of the property or properties inT
selected byK
.T[K]
permitsK
to be a type parameter, in which caseK
must be constrained to a type that is assignable tokeyof T
. Otherwise, whenK
is not a type parameter,T[K]
is resolved as follows:K
is a union typeK1 | K2 | ... | Kn
,T[K]
is equivalent toT[K1] | T[K2] | ... | T[Kn]
.K
is a string literal type, numeric literal type, or enum literal type, andT
contains a public property with the name given by that literal type,T[K]
is the type of that property.K
is a type assignable tonumber
andT
contains a numeric index signature,T[K]
is the type of that numeric index signature.K
is a type assignable tostring
andT
contains a string index signature,T[K]
is the type of that string index signature.Some examples:
An indexed access type
T[K]
permitsK
to be a type parameter that is constrained tokeyof T
. This makes it possible to do parametric abstraction over property access:Note: This description has been edited to reflect the changes implemented in #12425.