-
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
Mapped types #12114
Mapped types #12114
Conversation
# Conflicts: # src/compiler/checker.ts
This is very cool! It'll be great to be able to have stronger typings for all the sorts of utility-type function this will support. Just curious, would this allow for typings for a function like function omit<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, (keyof T) - K>; |
Can it handle non shallow transformations ? Like
|
@jkillian Probably after object-rest is in. @AlexGalays Nope, I checked out the branch. I didn't find a way this can be done. I tried 2 level partial, this is the best I could get: type DeepPartial<T> = {
[P in keyof T]?: T[P] | {[S in keyof T[P]]?: T[P][S]};
};
type A = { a: number, b: { c: boolean; d: string; } }
type B = DeepPartial<A>;
const b: B = { b: {c: {}} }; the second part: interface DeepPartial<T> {
[P in keyof T]?: T[P] | DeepPartial<T[P]>; // nope :(
} Also, There is no way to distinguish between objects and non-objects so everything will have This PR is really really great so far, I love it ❤️ ❤️ I don't think deep partial is possible at the moment. unless it is defined as a builtin modifier ( |
This might be irrelevant but I really cannot hold the urge to say here: the most exciting moment when using TypeScript is that after a magic pull request, all the sudden I can express a lot of semantics I couldn't before. That's the very freedom in programming world. This feature is so awesome. Thanks @ahejlsberg and TypeScript team. Make FrontEnd Great Again! |
@jkillian No active plans to support a type subtraction operator. It's unclear how such an operator would function across all types. That said, it might be possible to have a limited form that only works on primitive (and thus literal) types and unions thereof. That would suffice for the scenarios where you're subtracting keys from a @AlexGalays @alitaheri We currently restrict recursive use of a mapped type, but it's possible to lift that restriction. However, in actual use we'd still need some form of recursion limiter to avoid spiraling down an infinite series of nested mapped type applications. Ideally some sort of type parameter constraint or modifier you could use on a type alias for a mapped type to indicate that applications should short-circuit for certain types. That way you could for example declare @HerringtonDarkholme Thanks! Really appreciate your excitement for our work. |
This is highly exciting! And here I thought strict null check was the best thing since sliced bread, and then this came along. I think "mapped types" is a somewhat vague term, but seeing the syntactic form of this reminds me of dictionary comprehensions in Python, so perhaps "type comprehension" could be a more descriptive term? |
🚲 🏠 comment. Why not use type Partial<T> {
[for P in keyof T]?: T[P]
} this makes the iterative nature of the construct more prominent, and would allow for a future branch option using |
@mhegazy did you mean something like type SubstractType<T, K> {
[for P in keyof T if !(P in keyof K)]: T[P]
}
SubstractType<{name: string, age: number}, {age: number}> // {name: string} Type predicate for filtering property? |
typescript chose to sacrifice correctness for the sake of "convenience" and ease of learning for attracting as much untutored audience as possible, basically leveling everyone down to the least common denominator here is the list of hard choices in the making: https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93&q=is%3Aissue%20label%3A%22by%20design%22 |
Well, in my case I don't want users of my function to extend the base structural type with more properties; I want the full extent of properties declared upfront for typesafety. That rules out returning T & U for me. Also, be careful with If you do type A = { oops: ['1', '2', '3'] }
type B = { oops: 333 }
type C = A & B You would probably expect an Error, but instead it compiles just fine and the type of oops inside C is Partial and the likes seem to have fewer applications that previously thought in their current forms :( It still doesn't solve the React setState problem too : type State = { veryImportant: number }
// This compiles fine with all the mandatory flags (strictNullChecks, etc)
// if we use Partial<State>
// This is a huge invariant violation and can happen very easily for instance
// by setting `veryImportant` to a nullable variable.
this.setState({ veryImportant: undefined }) Let's start creating some focused bug tickets rather than complaining on this PR :p |
I'm trying to write typings for React's update helper and am struggling a bit. Here's what I have so far, trying to get things working for a subset of the library's functionality for now. I'm having a few issues which are noted in the code below: type Command =
{ $set: any; } |
{ $merge: {} } |
{ $apply(value: any): any; };
type Update<T> = { [P in keyof T]?: Update<T[P]> | Command; };
declare function update<T>(value: T, updateObj: Update<T>);
type Foo = {
a: number;
b: { b1: number; };
}
let bar: Foo = { a: 1, b: { b1: 2} };
// doesn't work without explicit <Foo>:
// The type argument for type parameter 'T' cannot be inferred from the usage. Consider specifying the type arguments explicitly.
// Type argument candidate 'Foo' is not a valid type argument because it is not a supertype of candidate '{ a: { $set: number; }; }'.
// Types of property 'a' are incompatible
// Type '{ $set: number; }' is not assignable to type 'number'.
update<Foo>(bar, { a: { $set: 3 } })
// also doesn't work without <Foo>
update<Foo>(bar, { b: { b1: { $set: 4 } } })
// shouldn't work at all, but does
update<Foo>(bar, { b: 7 }); Any ideas for ways to improve things? At the least, I think it may be an improvement over the current typings, but I wish it could be even better. Having to explicitly specify the type of |
You can accomplish it thusly: type Update<T, K extends keyof T> = { [P in K]: Command | NestedUpdate<T[P]> };
type NestedUpdate<T> = { [P in keyof T]?: Command | NestedUpdate<T[P]> };
declare function update<T, K extends keyof T>(value: T, updateObj: Update<T, K>); The problem is that once you get more than one layer deep, it stops actually validating the type. |
@PyroVortex Thanks! That works much better! I made an updated example demonstrating the now working cases and the failure case you mentioned in case anyone's interested |
@PyroVortex Seems a bit strange
Anyway, as seen in #12769 (comment), you would have to use the nightly build to get close to what you want without some bad side effects currently in 2.1.4. |
Ah, thanks for the reference to #12769 @AlexGalays. It looks like Anders' code is quite similar to what I had tried in this comment. Perhaps upgrading to the nightly is the right solution for me here |
Is it possible to get the following, get only those props from an object, that present in another object : function takeProps<Obj>
(props: {[K in keyof Obj]?: true}, obj: Obj) {
const newObj: any = {}
for (let key in props) {
newObj[key] = obj[key]
}
return newObj as {[K in keyof typeof props]: number}
}
takeProps({a: true, c: true }, {
a: 1,
b: 2,
c: 3
}) // => here I wan't be available only `a` and `c`, but get `a`, `b`, and `c` |
function takeProps<T, K extends keyof T>(props: {[P in K]: true }, obj: T): {[P in K]: T[P]} The above declaration has the behavior you are describing. |
@PyroVortex thanks very much) |
There is another question: is it possible to type a function that gets object map with values that are actually functors and returns mapped object applying interface A {
map: () => number
}
interface B {
map: () => string
}
let a: A = {
map: () => 1
}
let b: B = {
map: () => 'str'
}
function getMapped(sources: {[index: string]: {map: () => any}}) {
// implementation
const mapped: {[key in keyof typeof sources]: any} = {}
for (const key in sources) {
mapped[key] = sources[key].map()
}
return mapped
}
getMapped({a, b}).a // => should be 1
getMapped({a, b}).b // => should be 'str' |
Yes interface A {
map: () => number
}
interface B {
map: () => string
}
let a: A = {
map: () => 1
}
let b: B = {
map: () => 'str'
}
type MapsTo<T> = { map: () => T }
type MapSources<T> = {[K in keyof T]: MapsTo<T[K]> }
function getMapped<T>(sources: MapSources<T>): T {
const mapped: any = {}
for (const key in sources) {
mapped[key] = sources[key].map()
}
return mapped
}
getMapped({a, b}).a // => should be 1
getMapped({a, b}).b // => should be 'str' |
Any advice on how this can be accomplished a "functor" with two mapping methods : const a = {
mapR: () => 1,
mapT: () => true
}
const b = {
mapR: () => 'str',
mapT: () => false
}
type MapsTo<T, R> = {
mapT: () => T,
mapR: () => R
}
type MapSources<T, R> = {[K in keyof (T & R)]: MapsTo<T[K], R[K]> }
function getMapped<T, R>(sources: MapSources<T, R>): { T: T, R: R } {
const mapped: any = { T: {}, R: {}}
for (const key in sources) {
mapped.T[key] = sources[key].mapT()
mapped.R[key] = sources[key].mapR()
}
return mapped
}
getMapped({ a, b }).R.a // => should be 1
getMapped({ a, b }).R.b // => should be 'str'
getMapped({ a, b }).T.a // => should be true
getMapped({ a, b }).T.b // => should be false |
@whitecolor you might want to try the gitter channel for typescript Anyway, here is the solution: http://bit.ly/2od3xP5 The reason why the original doesn't work is because all the keys of T & R are not necessarily present in both T and R, so nothing is known about T[K] and R[K]. Moving the & operator to the argument level ensures that the argument is a source of both kinds of "maps" |
This PR introduces Mapped Types, a new kind of object type that maps a type representing property names over a property declaration template. In combination with index types and indexed access types (#11929), mapped types enable a number of interesting and useful type transformations. In particular, mapped types enable more accurate typing of intrinsic functions such as
Object.assign
andObject.freeze
as well as APIs that map or transform shapes of objects.A mapped type takes one of the forms
where
P
is an identifier,K
is a type that must be assignable tostring
, andT
is some type that can useP
as a type parameter. A mapped type resolves to an object type with a set of properties constructed by introducing a type parameterP
and iterating it over the constituent types inK
, for each suchP
declaring a property or index signature with the type given byT
(which possibly referencesP
as a type parameter). WhenP
is a string literal type, a property with that name is introduced. Otherwise, whenP
is typestring
, an index signature is introduced.Type relationships involving mapped types are described in #12351. For information on type inference involving mapped types, see #12528 and #12589. For information on preservation of property modifiers with mapped types, see #12563.
The following four mapped types are predefined in lib.d.ts as of #12276:
Some functions that use the above types:
And some code that uses the functions:
The
mapObject
example above shows how type inference can be used for mapped types. When inferring from an object typeS
to a mapped type{ [P in K]: T }
,keyof S
is inferred forK
andS[keyof S]
is inferred forT
. In other words, a literal union type of all property names inS
is inferred forK
and a union of all property types inS
is inferred forT
.Another common pattern:
Related issues include #1295, #2710, #4889, #6613, #10725, #11100, #11233.