-
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
Rest type #13470
Conversation
Suggestion:
|
Doesn't this break some design consistency? All value argument lists are now delimited with braces This PR adds a Since, I regard |
Previously, rest types were only allowed to be identifiers by mistake. Also remove unneeded parsing support for rest types that got checked in by mistake.
1. Object rest checking allows nested generics now. 2. Use getLiteralTypeFromText instead of createLiteral in order to intern literals correctly.
@Igorbek I didn't call it out sufficiently in the proposal text above, but the second argument of [1] The reason that the second argument is not generic is that it's impossible to produce a generic object rest — you have to hard-code the names of properties that will be removed in the object destructuring/binding. @tinganho I don't have a strong design opinion one way or another on the syntax. The only constraint is that we expect rest and spread types to be used rarely, especially rest types, so I don't want to use (for example) How about we gauge the community's preference by using 👍 to your comment to indicate |
interface FooBar {
foo: string;
bar: string;
}
interface Foo {
foo: string;
}
interface Bar {
bar: string;
} would |
@sandersn thanks for your comment. Sorry, that I didn't discovered your initial proposal earlier(I just read the spread part). I think, I understand why you use lowercase |
@sandersn I see what you mean. But isn't that the case for computed properties? const b = "b";
let c = { [b]: b, a: 10 }; // c is { [x: string]: string | number, a: number; }
let { [b]: b1, a: a1 } = c; // b1 is any; a1 is number so should this be restricted then? const b = "b";
let rest = { a: 10 };
let c = { [b]: b, ...rest };
let { [b]: b1, ...rest1 } = c; // BTW, this is allowed by the spec and Babel supports it and generic case: function destructure<T, U extends keyof T>(src: T, name: U): { value: T[U]; rest: rest(T, U); } {
const { [name]: value, ...rest } = src;
return { value, rest };
} just thinking out loud |
@rozzzly Yes. @tinganho I would much prefer a general solution too; I tried to make something work with mapped types already. Maybe you can use these problems that I ran into to come up with something:
|
Although I don't love the proposed rest syntax I don't really care for the generic type syntax either in this case. The advantage of not introducing new syntax is just that it would allow a better syntax to be introduced, preferably one that covered more type operators, in the future without causing confusion because it would be a shorthand or sugar as opposed to an alternate manifest form. |
@sandersn So I submitted my idea to the issue tracker, though it is very rough. Though my initial idea was to solve typings for ORMs, but it seems it can apply to type Rest<T, ...S extends string[]> => {
type Type;
for (KT in T) {
type IsInS = false;
for (KS of S) {
if (S[KT] != undefined) {
IsInS = true;
}
}
if (!IsInS) {
Type[KT] = T[KT]
}
}
return Type;
} |
function concrete(tricky: keyof (typeof c)) {
return destructure(c, tricky)
} This might remove
The typescript checker should be compared to flow, which doesn't support the generic case either and defaults to returning I will add an error instead. I don't like silently failing to remove properties that people asked to remove. @aluanhaddad I hoped that introducing new, bad syntax would have nearly the same benefits as not introducing new syntax at all, with the added benefit that the syntax for generic types stays consistent. |
So, does the rest operator finally allow the strict typing of applying declare function map<A extends Array<T>, T, U>(input: A | [void], operator: (t: T) => U): Array<U> & { [P in keyof rest(A, keyof Array<T>)]: U };
const x = map([1,2,3], (x: number) => x.toString()); // x should have type [string, string, string], or at least one that has all the same semantics Alternatively, if you want to generate invariant tuples: function asInvariantTuple<A extends Array<any>>(input: A | [void]): rest(A, keyof []) & { readonly length: A['length'] } {
return input;
}
const y = asInvariantTuple([1, 2, 3]); // y has type { 0: number, 1: number, 2: number, length: number } |
It is an error with --noImplicitAny, unless the source type is also any.
Also update baselines
You are right that I claimed that the only useful way to produce a rest type is from an object rest destructuring. The reason is that the ES spec defines both object rest and object spread as removing call signatures and non-own, non-enumerable properties, which Typescript approximates as class methods. That means that basic identities just break down:
I considered adding a non-ES-based type similar to the intersection type called the difference type and then defining |
Ah, yes, you're right. This rules are breaking basic identities.
As an example of generic HOC could be: const withProps =
<P>(props: P) =>
<T /* missing constraint that P extends T */>(Component: Component<T>) =>
(innerProps: rest(T, keyof P)) =>
<Component {...{innerProps, ...props}} />
const Sample = (props: { a: string; b: string; }) => <div>{a + b}</div>;
<Sample a='hello, ' b='world' />
const HelloSample = withProps({ a: 'hello, '})(Sample);
<HelloSample b='world' /> An alternative approach could be using something like Mapped conditional types #12424 where properties could be filtered out or replaced with some other type (like, make some members optional). |
And one more alternative: const withProps =
<P>(props: P) =>
<T>(Component: Component<{...T, ...P}>) =>
(innerProps: T) =>
<Component {...{innerProps, ...props}} /> This is perfect typing, but the issue here is that type |
@sandersn Maybe I should have not included |
Is there any way of seeing this merged at least without support of generics? |
How about using
|
Perhaps this could be solved with a syntax closer matching JS (avoids syntax clash, keeps learning curve low) with the type-level destructuring explored in #10727: Maybe then one might grab them from objects and/or unions using a spread as well, hopefully allowing for straight-forward support for generics: // destructure object
type Obj = { a: 1, c: 3 };
type { ...Obj, ...Rest } = { a: 1, b: 2 };
// Rest: { b: 2 } Note
Alt.: destructure unioned keys type Obj = { a: 1, c: 3 };
type Keys = keyof Obj; // 'a' | 'c'
type { ...Keys, ...Rest } = { a: 1, b: 2 };
// Rest: { b: 2 } Notes:
Not just |
Tried for a bit more to do a union difference operation using today's syntax. Unlike the current approach, this would handle the generics use-case as well. // define some helpers...
type Obj<T> = { [k: string]: T };
type SafeObj<O extends { [k: string]: any }, Name extends string, Param extends string> = O & Obj<{[K in Name]: Param }>;
type SwitchObj<Param extends string, Name extends string, O extends Obj<any>> = SafeObj<O, Name, Param>[Param];
type Not<T extends string> = SwitchObj<T, 'InvalidNotParam', {
'1': '0';
'0': '1';
}>;
type Union2Obj<Keys extends string> = { [K in Keys]: K };
type Union2Keys<T extends string> = Union2Obj<T> & { [k: string]: undefined };
type UnionHas<Union extends string, K extends string> = ({[S in Union]: '1' } & { [k: string]: '0' })[K];
// okay, let's try this...
type UnionDiff<Big extends string, Small extends string> =
{[K in Big]: { 1: Union2Keys<Big>[K], 0: undefined }[Not<UnionHas<Small, K>>]}[Big] /*!*/;
type test1 = UnionDiff<'a' | 'b' | 'c', 'b' | 'c' | 'd'>;
// 'a'|'b'|'c' instead of 'a' :( This yields Second attempt, separate the steps... // hm, let's separate the steps...
type UnionDiff2<Big extends string, Small extends string> =
{[K in Big]: { 1: Union2Keys<Big>[K], 0: undefined }[Not<UnionHas<Small, K>>]} //[Big] /*!*/;
type A = UnionDiff2<'a' | 'b' | 'c', 'b' | 'c' | 'd'>;
// now manually do that second step...
type Big = 'a' | 'b' | 'c';
type B = A[Big];
// "a" (yay!), or `"a" | undefined` if `strictNullChecks` are enabled. This returns either The Edit: included needed helpers. |
How about trying type UnionDiff2<Big extends string, Small extends string> =
{[K in Big]: { 1: Union2Keys<Big>[K], 0: never }[Not<UnionHas<Small, K>>]} //[Big] /*!*/;
type A = UnionDiff2<'a' | 'b' | 'c', 'b' | 'c' | 'd'>;
// now manually do that second step...
type Big = 'a' | 'b' | 'c';
type B = A[Big];
let testYes: B = "a"; // typechecks
let testNo: B = "b"; // fails PS nice type level magic. Kind of sad this works only for string literal types, not arbitrary types due to index signature limitation though. |
@jaen: progress! thanks for that! 😄 I'll concede the limitation is unfortunate, yeah. Out of curiosity though, what's your use-case?
I couldn't think of much either, hence I'm all the more curious. :) |
Depends on use case of what you're asking about. If we're talking about subtracting of arbitrary types from an union (which is what led me here) it's fairly simple, our codebase uses tsmonad with strict null checks leading to some annoying issues, for example: const maybe: <T>(value: T) => Maybe<T> =
(value) =>
!!value
? Just<T>(value)
: Nothing<T>();
maybe(possiblyNull)
.fmap(x => x + 2); // complains about `x` being possibly null, even though
// `fmap` will by definition be only called with inhabited
// `Just` values
.valueOr(10) which could be alleviated with something like either of (or preferably both): const betterMaybe: <T>(value: T) => Maybe<T - null | undefined> = …;
// alternatively by lifting non-null assertion to type level
const betterMaybe: <T>(value: T) => Maybe<T!> = …; Is that a satisfactory answer? |
@jaen: hmm. what if you'd write say Edit: oh, I know this isn't exposed on the type level either, but have you tried doing something like in the type guards and type assertions example? It looks like if you use a type guard that way that may suffice to have TS recognize the |
Yeah, I am aware of the non-null assertion As for your suggestion — this will work I assume, but will require me forking tsmonad and modifying the source, which I wanted to avoid. Being able to do this on the type level has the added benefit on being able to do it in a local typings file to shadow the library's type. So that's my use case, I guess — this can be done otherwise, by writing down a non-null assertion at each use site or forking or modifying the library to work with strict null checks, but both feel fairly inelegant to me. |
@jaen: From the TS team's perspective, I suppose a general solution was considered fairly involved, in the sense it'd require introducing negation types ('a type that is not |
Hm, I guess I could consider a PR. I thought the library was dead (had improvements over last version in the repo, but no officially released package), but now that I checked it's not, so it might make sense to change that upstream, I guess. Well, I'd probably prefer a more generic solution, since while lifting the Do you have any link explaining why the negation type would be needed? I would assume you can remove from a union by equality (which types already have) so I'm not sure why some not- |
Putting things together based on @nirendy's solution to work around the glitch by cutting things into steps with generics defaults: type Obj<T> = { [k: string]: T };
type SafeObj<O extends { [k: string]: any }, Name extends string, Param extends string> = O & Obj<{[K in Name]: Param }>;
type SwitchObj<Param extends string, Name extends string, O extends Obj<any>> = SafeObj<O, Name, Param>[Param];
type Not<T extends string> = SwitchObj<T, 'InvalidNotParam', {
'1': '0';
'0': '1';
}>;
type Union2Obj<Keys extends string> = { [K in Keys]: K };
type Union2Keys<T extends string> = Union2Obj<T> & { [k: string]: undefined };
type UnionHasKey<Union extends string, K extends string> = ({[S in Union]: '1' } & { [k: string]: '0' })[K];
export type UnionDiff_<Big extends string, Small extends string> =
{[K in Big]: { 1: Union2Keys<Big>[K], 0: never }[Not<UnionHasKey<Small, K>>]}//[Big];
export type UnionDiff<
Big extends string,
Small extends string,
Step extends UnionDiff_<Big, Small> = UnionDiff_<Big, Small>
> = Step[Big];
type TestUnionDiff = UnionDiff<'a' | 'b' | 'c', 'b' | 'c' | 'd'>;
// ^ 'a' |
Should this be closed? |
On hold for now |
Add rest types. They implement the semantics of object rest as detailed in the second part of the proposal #10727. I have copied and updated that proposal here.
Notes:
Rest types
The rest type is the opposite of the spread type. It types the TC39 stage 3 object-rest destructuring operator. The rest type
rest(T, 'a' | 'b' | 'c')
represents the typeT
after the propertiesa
,b
andc
have been removed, as well as call signatures and construct signatures.A short example illustrates the way this type is used:
Syntax
The syntax is
rest(T, U)
whereT
is any type andU
isstring
or a union of string literals (includingnever
and single string literals).Type Relationships
rest(A, never)
is not equivalent toA
because it is missing call and construct signatures.rest(A | B, 'a')
is equivalent torest(A, 'a') | rest(B, 'a')
.rest(A, string)
is equivalent to{}
.rest(rest(A, 'a'), 'b')
is equivalent torest(A, 'a' | 'b')
and therefore also torest(rest(A, 'b'), 'a')
.Assignment compatibility
rest(T, 'x')
is not assignable toT
.T
is assignable torest(T, 'x')
becauseT
has more properties and signatures.rest(T, V)
is assignable torest(U, W)
ifT
is assignable toU
andV
is assignable toW
.Properties and index signatures
The type
rest(A, P)
removesP
fromA
if it exists. Otherwise, it does nothing.A
retains its index signatures unlessP
isstring
.Call and Construct signatures
rest(A)
does not have call or construct signatures.Precedence
Rest types are just below the keyof operator in precedence.