Skip to content
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

fleshing out type operators (discussion) #16392

Closed
42 of 71 tasks
KiaraGrouwstra opened this issue Jun 9, 2017 · 54 comments
Closed
42 of 71 tasks

fleshing out type operators (discussion) #16392

KiaraGrouwstra opened this issue Jun 9, 2017 · 54 comments
Labels
Discussion Issues which may not have code impact

Comments

@KiaraGrouwstra
Copy link
Contributor

KiaraGrouwstra commented Jun 9, 2017

This is a discussion thread where I'd like to give a high-level overview of the type-level operations (as opposed to expression-level) that we can and can not yet do today.

This differs from the TS roadmap by identifying holes, while complementing the issues list by trying to show some of the bigger picture, the goal being to see what issues tie into which points, and how we could address them.

I'd like stimulate discussion on how we could fill the holes here; for all I know there are holes we can find solutions to with no changes to TS!

Below is my list of imaginable basic type operations. The reason I focus on these is that, with basic operators down, most more complicated use-cases could be addressed simply by combining these. Names are based on my implementations here.

Additions / corrections / related issues / comments welcome!

Operations:

Built-in operators:

  • union |: allow either of two types. also helps get the more lenient of two types, i.e. T | never -> T. you'll encounter this in type inference since optional params yield | undefined types. no known warts.
  • intersection &: get the stricter of two types, i.e. T & never -> never. shouldn't need this very often. also helps combine two objects. warts:
    • overlapping keys when combining objects will & their contents too (alt: Overwrite / MergeAll)
  • keyof: create a union of string literals from a type's keys. warts:
    • returning a string union means number literal keys get converted to string literals
    • returning a string union means symbol keys get ignored
    • when a string index is present, there is no known way to get the individual string keys
    • somehow also gives prototype keys for array types, yet not for object types
  • in: construct an object type based on a union of strings (keys) and corresponding calculated values based on these. warts:
    • since this is based on strings, anything else, such as numbers, symbols, or indices, needs to be specified separately (-> &).
  • member access: get the type at a certain index of an object/tuple type (considering intersections as a single object, getting multiple result for union'd objects). warts:
    • there is ambiguity w.r.t. whether the prototype or the index should trump. it currently lets the prototype trump, even if you wanted it not to, meaning you're likely to get unexpected behavior for member access operations for e.g. toString.
    • does implicit conversion between numeric literals and string literals, not unlike JS. not a problem though. :)

& is a bit less straight-forward from the rest in its use-cases:

  • a way to make impossible types like string & number (uses?)
  • a redundant way to write never (T & never), which gets more useful given conditionals, but then you could just use those to conditionally produce never right away
  • a poor man's Overwrite / MergeAll (inferior in its behavior intersecting types in overlapping keys, which poorly reflects actual JS). if you know keys won't overlap though, it's great since it's short, built-in and performant.
  • 'get the most specific type of these two': haven't found use-cases for this, but could also be done given Matches
  • add a symbol to an existing object
  • have an index type with keys not bound to sub-classing it

Boolean operations:

  • Not, And, Or, Eq, Neq

Note: these can currently be implemented through string literal representations. It would be possible to convert these to boolean literals (StringToBool), but cannot yet map boolean literals to these (BoolToString) or other values for that matter.

Array (tuple) operations

Unary:

  • TupleLength: check the length of a given tuple type.
  • ArrayProp: get the element type for a homogeneous array type (similar for extracting generics from other parameterized types)

Binary:

  • [x] TupleProp: get the type at a certain index for a tuple/array type. just T[I].
  • TupleHasIndex: check whether a tuple type contains a given index.
  • TupleHasElem: check whether a tuple type contains a given type among its elements. This could be done given TypesEq.
  • concatenate / append / prepend. possible today using numeric objects (compatible with ArrayLike), but could become possible for tuple types natively with the variadic kinds proposal at Proposal: Variadic Kinds -- Give specific types to variadic functions #5453. There has been talk there this would also depend on Proposal: strict and open-length tuple types #6229.
  • destructuring tuples: see above
  • difference: remove indices in one tuple type from another tuple type. see above.
  • Vector: create a tuple type for a given element type plus size.

Advanced:

  • array iteration, used for e.g. TupleLength
  • reduce: the function needs ReturnType for its dynamic reducer functions; otherwise doable using iteration. see Proposal: add a built-in reduce function on the type level #12512.
  • map over tuples: doable now through numerical objects for fixed conditions; also needs ReturnType in case the mapping function is given as a function (e.g. map itself).

object operations

Unary:

  • ObjectLength: check the length (number of keys) of a given heterogeneous object type. doable given UnionLength or (object iteration + Inc).

Binary:

  • ObjectProp (need to test): get the type at a certain index for an object type. Normally one would just use T[K], which offers the desired behavior if one expects prototype methods like toString to prioritize the prototype over the string index. If one instead expects these to trigger the string index, you'd want this instead.
  • ObjectHasStringIndex: check whether an object has a general string key, e.g. [k: string]: any.
  • ObjectHasNumberIndex: accessing it works or throws, not sure how to check presence though.
  • ObjectNumberKeys: a number variant of keyof. could be pulled off given union iteration (Partial -> iterate to filter / cast back to number literals)... but still hard to scale past natural numbers.
  • ObjectSymbolKeys: a Symbol variant of keyof. no clue how to go about this unless by checking a whitelisted set such as those found in standard library prototype. this feels sorta useless though.
  • ObjectHasKey: check whether a heterogeneous object type (-> like { a: any } as opposed to { [k: string]: any }) contains a given key.
  • ObjectHasElem: check whether a heterogeneous object type contains a given type among its elements. This could be done given TypesEq.
  • Overwrite: merge objects, overwriting elements of the former by that of the latter. see #12215.
  • Omit (#12215): remove certain keys from a given object type.
  • ObjectDifference: remove all keys from an object that are part of a second object.
  • IntersectionObjects: filter an object to the keys also present in another object.
  • FilterObject: can be done already for fixed conditions; using a predicate function needs ReturnType

Advanced:

  • map over heterogeneous objects: probably just needs ReturnType.
  • object iteration: useful for e.g. ObjectToArray. This could enable union iteration, or the other way around.
    • One strategy that comes to mind relies on converting keys to tuple (given UnionToArray) then using array iteration.
    • Alternatively, break string literals into characters, convert to numbers, convert objects to a nested version with one key at each stage using key sort, which could then be traversed in order... Nope, no member access on string literals.

Type operations

Type checks (binary):

  • Matches: check whether a given type matches another type (inclusive, e.g. true for string and string). This could be done given ReturnType.
  • TypesEq: check whether two types are 'equal', that is, A satisfies B and vice versa. This could be done given Matches.
  • InstanceOf: check whether a given type represents a subset of another type (-> exclusive match). This could be done given Matches.
  • PrototypeOf: get the prototype (-> methods) of a type. Partial helps, though Symbol-based keys get killed.

Type casts (unary):

  • StringToBool: can be implemented manually given the limited options, mapping desired keys to true / false, potentially having anything else fall back to undefined / boolean. string literals are used in the boolean operators above, while boolean literals are useful in e.g. type guards (expression-level if/else).
  • BoolToString -- mapping from non-strings could be done given ReturnType.
  • StringToNumber -- convert a numerical string literal to an actual number literal, doable using a whitelist (doesn't scale well to higher numbers).
  • NumberToString -- convert a number literal to a numerical string literal, doable using a whitelist (doesn't scale well to higher numbers).
  • UnionToObject -- use a union of string literals as object keys, possible with e.g. { [P in Union]: P }.
  • UnionToArray: could be done given e.g. union iteration.
  • ObjectToArray: could be useful if converting tuples types to number-indexed object types, do further operations, then convert back. likely needs object iteration.
  • ObjectKeysToUnion -- keyof does this.
  • ObjectValsToUnion: just plug the keys back into the object
  • TupleToObject: convert a tuple type to an object type (both number/string indices work), cleaning out prototype methods.
  • TupleToUnion: convert a tuple type to a union of types.
  • TupleIndicesToUnion: get the indices of a tuple type as a union of numerical strings.

Union operations

Unary:

  • a way to access union elements, e.g. going from "a" | "b" | "c" to "a". this could enable union iteration using Diff if they're all string literals, which in turn could enable object iteration. or the other way around.
  • IsUnionType -- solvable today only for unions consisting of known sets of keys, see my Indeterminate; a proper solution could be made using union iteration or a way to access arbitrary / random elements (e.g. with conversion to tuple type)
  • UnionLength: check the length of a union, i.e. how many options it is composed of.

Binary:

  • union: A | B
  • UnionHasKey: check whether a union of string literals contains a given key.
  • UnionHasType: general case, check whether a union of arbitrary types contains a given type.
    • could be achieved using TypesEq. plugging a union into it should return e.g. "0" | "1" in case it contains a match -- at that point UnionHasKey works.
  • IntersectionUnions: get the intersection of two union types, possible today given unions of string literals.
  • DifferenceUnions: subtract any keys from one union from those contained in another union.
  • UnionContained: verify whether one union is fully contained in another.

Advanced:

  • union iteration: helps implement UnionToArray, IsUnionType. could be achieved given UnionToArray or a way to access elements from a union. This could enable object iteration, or the other way around.

intersection operations

function/parameter operations

operations on primitives (string/number/boolean literals)

These are currently considered out of scope, see #15645.

That said we can do a bit with natural numbers:

  • Numbers: Inc, Dec, Add, Subtract, Mult, Pow, DivFloor, Modulo, comparators: Gt (>), Lt (<), Gte (>=), Lte (<=)

Strings:

  • member access on string literals
  • appending string literals

Progress:

Type Member Access Manipulation Iteration
Tuple ⭕ (as numerical objects until #5453)
Object
Union (of string literals*)
Function ❌ (#6606) ❌ (#5453) n/a
Bool n/a ⭕ (as strings) n/a
Number n/a ⭕ (low-ish natural numbers) n/a
String ❌ (#15645) n/a

*: union operators are pretty much limited to unions of string literals as it stands, as the only basic operators on unions (in + member access) both operate exclusively on these.

Not listed: type-level type checks (also need #6606)

Top features needed:

@ikatyang
Copy link
Contributor

ikatyang commented Jun 9, 2017

There are several dead links: #12215(x2), #13470, #16114, their /issues/ are missing.

@KiaraGrouwstra
Copy link
Contributor Author

@ikatyang fixed them, thanks!

@KiaraGrouwstra
Copy link
Contributor Author

Update: I fleshed out operations on number literals (if restricted to whitelisted natural numbers), and realized that, although we can't really manipulate tuple types without ... operator, we can actually iterate over these to reconstruct them as numerical objects satisfying the ArrayLike interface (numbers + length), which could suffice as a workaround to operate on 'tuples' for now.

The implication here is that the vast majority of the remaining unresolved challenges now suddenly hinge on a single outstanding feature request (#6606).

@KiaraGrouwstra
Copy link
Contributor Author

As Playground became less suitable as things grew, I now moved the types into a repo.

@DanielRosenwasser DanielRosenwasser added the Discussion Issues which may not have code impact label Aug 2, 2017
@SimonMeskens
Copy link

Stellar work. I'm going to pour over this when I get home tonight. Maybe we should turn this into a test suite too and get it accepted into Typescript, so there's no regressions?

@KiaraGrouwstra
Copy link
Contributor Author

KiaraGrouwstra commented Aug 3, 2017

@SimonMeskens: I'd certainly love to improve lib.d.ts, though an official blessing might be more justifiable for types that could already be used to help type it than for the ones that have yet to be applied there. I investigated opportunities to improve it earlier, but haven't found many that'd work out today yet. Specifically:

tl;dr: hopefully, but things might not be mature enough yet.

That said I did see two types failing I'd sworn worked before, specifically Indeterminate and Ramda attempt PathOrFn. I might need to look into those again.

@SimonMeskens
Copy link

It feels like we should be able to do ReturnType right now. What's stopping us?

@KiaraGrouwstra
Copy link
Contributor Author

KiaraGrouwstra commented Aug 4, 2017

@SimonMeskens: try it!

interface MyFn {
  (s: string): string;
  (b: boolean): boolean;
}
// ^ example of a problematic function: overloads. similar for generics.
type Ret<F extends (...args: any[]) => R, R> = R;
type Bar = Ret<MyFn>;
// ^ error: Generic type 'Ret' requires 2 type argument(s).
declare function ret<R>(f: (...args: any[]) => R): R;
let baz = ret(null! as MyFn);
// ^ expression level, can't be composed into bigger types
// -> boolean. other option got ignored?

In function declarations, we can do an easy version naively extracting a return type, see the snippet below; a type-level-only version fails meaning we can't really put this to use in other types.
It also fails if the function's return type depended on generics, overloads, or this binding types.
There are a dozen use-cases that depend on the ability to calculate a return type appropriate for given inputs though, which is a topic that could be resolved with that 6606.

@SimonMeskens
Copy link

Ah yes, it's basically the issue I talked about on the dynamic function type issue. You can create such a function (well, once 2.5 lands to fix a few of the mapped type bugs), but I don't think TypeScript could ever support complex functions (generics, overloads, not sure about this bindings, you can probably support those eventually) dynamically. You could specifically write a return type construct as in 6606 of course, but the general case is probably not possible, due to the way generics are constructed.

@KiaraGrouwstra
Copy link
Contributor Author

You can create such a function (well, once 2.5 lands to fix a few of the mapped type bugs)

Could you show a snippet of how you'd go about it with that?

but I don't think TypeScript could ever support complex functions (generics, overloads, not sure about this bindings, you can probably support those eventually) dynamically.
You could specifically write a return type construct as in 6606 of course, but the general case is probably not possible, due to the way generics are constructed.

If you mean the awkward Ret attempt in that snippet, yeah.
If you mean 6606, it already calculates types of return values based on all of that for function calls made on the expression level. It's just about getting that existing operator (() / <>()) exposed in type land.

@SimonMeskens
Copy link

Could you show a snippet of how you'd go about it with that?

Not really, because I ran into several bugs trying to make it. The basic idea is that you specify arity by hand and the compiler will complain if the arity is incorrect. Unless we get some way to pattern match types, we can't make the compiler infer arity unfortunately. The arity can be somewhat inferred through a number of overloads at call-site.

Once 2.5 lands, I'll try to produce this type.

I'm somewhat skeptical of 6606, because if it would actually provide a fully working return type, it would be more powerful than Haskell's compiler, if I understand correctly, and I simply don't see how Typescript's generic system would give rise to such a construct. I just returned from a vacation, so my brain is currently too fried to provide an example, I'll try to do so later.

@KiaraGrouwstra
Copy link
Contributor Author

KiaraGrouwstra commented Aug 5, 2017

@SimonMeskens:

If you want you can try 2.5 nightly outside of playground. Heck, even if the code doesn't work yet, the concepts would still be interesting anyway. We'd have progress even if just for arity 0 it works purely on the type level.
On another note, alternatives would be reminiscent of #14400.
Arity I guess could be addressed by either 5453 (capturing variadics) or 6606 (overloads) itself.

I'm somewhat skeptical of 6606, because if it would actually provide a fully working return type, it would be more powerful than Haskell's compiler, if I understand correctly, and I simply don't see how Typescript's generic system would give rise to such a construct.

TypeScript has been doing type literal computations that Haskell never bothered with, e.g. property access on tuples / heterogeneous objects using number / string literals. Ditto for boolean literals, see the type guards in the tutorial.
Note that none of that required generics in the first place.

I'd asked a few Haskell friends this same question before, and their response was just kinda that it wouldn't have as much use for it, in the sense its tuples lacked number-based access, and that heterogeneous objects with string index access basically also lacked a Haskell equivalent.

@SimonMeskens
Copy link

I see your point now, you want to be able to infer the return type, not just typecheck it. I don't think that's possible without something like #14400.

Honestly, I feel like not having #14400 is holding the language back. That's probably my number one feature request, more inference on generic arguments.

@KiaraGrouwstra
Copy link
Contributor Author

Well, #14400 itself wouldn't address overloads/generics or the like, while we can already get the return type like that in function definitions already (ret example above). In that sense it'd be a relatively smaller step. If we could get 6606 then from what I can see that'd cover it.
Then again I also just don't know how implementing 14400 would actually work.

@SimonMeskens
Copy link

#14400 Already exists in C# and many other, simpler languages. It's a small subset of what you want to cover with 6606. On the other hand, 14400 opens up a bunch of doors all over the place, not just for return types.

@KiaraGrouwstra
Copy link
Contributor Author

KiaraGrouwstra commented Aug 5, 2017 via email

@SamPruden
Copy link

I've just posted a suggestion for type declaration overloading at #17636. @tycho01, I wonder if you'd like to have a look and see how useful you think that might be to realising some of the stuff in this issue? I'll take more of a look myself when I have the time, but I'm pretty sure @17636 would open up BoolToString at least, maybe many more.

@KiaraGrouwstra
Copy link
Contributor Author

KiaraGrouwstra commented Aug 6, 2017

@TheOtherSamP:

Going by my list in 6606:

overloading covers:

  • operate on boolean literals
  • unwrapping like promised
  • constraints, incl. (non-) union
  • type subtraction
  • type checking
  • checking index presence

not covered is function application:

  • angular factories
  • higher-order functions (compose, curry, bind, reduce, filter, tuple map, ...)
  • lenses

Going by my list here:

  • type manipulation: ReturnType, BoolToString, Matches / TypesEq / InstanceOf, ObjectHasElem, TupleHasElem, conditional errors, pattern matching, constraints, ObjectHasStringIndex, ObjectHasNumberIndex.
  • function stuff: map over tuples / heterogeneous objects, reduce, filtering objects by predicate

tl;dr:

  • when we do actually need to do stuff with functions (because we get them in through params) we would, for better inference, need a way to deal with those.
  • for every other use-case you'd be correct I just brought in functions to address this overloading, and that an alternate method of doing overloads could also cover that.

Edit: one potential con of your type overloading approach is that it may require adding external types, while a function approach might also allow anonymous functions. From what I can see this should not necessarily be a deal-breaker for any of its use-cases though.

@SamPruden
Copy link

@tycho01 That's great, thanks! That's a big list of stuff that works. Honestly, even when we get #6606 I still think it would be nice to have both. Type declaration overloads seem neater for these things, having to bring functions into those operations feels like a bit of a hack. I think I'd be in favour of having both features in the language alongside each other.

Would you mind copying that list over to #17636? I think you've made a pretty decent argument in favour of it being worth considering there, far more exhaustive than mine, it would be nice to have it in that issue. Alternatively I could steal it and edit it into the main post I suppose.

@KiaraGrouwstra
Copy link
Contributor Author

@TheOtherSamP: I found a few kinks:

  1. Functions can capture inferred types in generics based on provided param types. This is relevant to obtain references for things we cannot otherwise refer to:
  • parameter / return types of unapplied functions
  • constituents of unions (/ intersections?)

Examples depending on this:

This could also be tackled through #14400.

* Given 6606 that is not how you wanna calculate return types (doesn't consider param/generic types), but it's a bit of a shorter demo than extracting params of an unapplied function into a tuple type.

There are anonymous functions, yet not (yet) anonymous types. As a result, a 6606-based approach allows type 'currying'.
As an example, type checks with 6606 would use e.g.:

export interface isT<T> {
  (v: T): '1';
  (v: any): '0';
}
export type Matches<V, T> = isT<T>(V);

With #17636 that might look a bit like:

type isT<T, V extends T> = true;
type isT<T, V> = false;

The 6606 approach might allow terse derivatives e.g. type isBool<V> = isT<boolean>, for which 17636 might still need to manually put the overloads in external types.
That's doable for a known type like boolean, but for a type from a generic, externalizing is not possible. I'm not yet confident w.r.t. what use-cases might require that.

tl;dr aside from doing stuff with actual functions, 17636 covers the use-cases of 6606 with a few (fixable) holes, namely capturing inferred types (#14400) and anonymous types (not aware of proposals).

So yeah, I'll post there too. I think I should have the differences down now.

@SamPruden
Copy link

I'm not sure if this is new, but I found a cool trick that allows some basic type switching (and more cool things) by augmenting some global interfaces. It's a bit hackish, but I think it's okay, I haven't hit any problems with it yet. It also allows the implementation of type DeepReadonly<T> which is awesome, but I'm not yet 100% sure whether that's relying on a bug or not.

Figured it was worth noting here in case it's new and opens up anything else on this list.

I don't know if there's a fancy way to link to gists, but here: https://gist.github.com/TheOtherSamP/ab0c7305d241cee4c7f0452f11a4d1f1

@KiaraGrouwstra
Copy link
Contributor Author

@TheOtherSamP: makes sense. In that case I guess your question on why the type-checker was cool with it sounds pretty legitimate.

@SamPruden
Copy link

@tycho01 Yeah, there actually seems to be a larger issue (though it's useful so far, maybe feature?) with constraints not being checked properly in nested/recursive types. Although now I think about it, I wonder if something is becoming any somewhere along the way in the checks and allowing it.

@SamPruden
Copy link

Although this toy example to recreate the issue actually does get caught, so whatever that behaviour is is a bit more subtle.

type MustBeNumber<T extends number> = T;
type KeyofWhatever<T> = {[K in keyof T]: MustBeNumber<T[K]>}; // Errors

I'll have to fiddle about and see if I can recreate. I think I also found another bug (something was working inline, but not through a layer of type declaration abstraction) earlier too, but I just went a completely different route around it, so I'll have to see if I can recreate that for bug report.

@KiaraGrouwstra
Copy link
Contributor Author

@TheOtherSamP: this is one of the things I find tougher about type-level programming; you can't just like cram in some logging statement for debugging... I do wonder how people with more experience like @gcnew are going about that, assuming it doesn't require full familiarity with the compiler so as to run it through that with breakpoints.

@SamPruden
Copy link

@tycho01 Yeah absolutely, there's a lot of trial-and-error involved in my process here, rapid iteration, and stuff like this sitting around for debug-by-tooltip checking:

type H = DeepReadonlyObject<DummyInterface>;

// type A = HasProperty<{test: 5}, "test">;
type IsArrayTest = IsArray<Array<string>>;

type C<T> = IsArray<T>;
type D = SpecialTypeOf<string>;
type E = HasSpecialType<string[]>;
type F = IsSpecial<string, "array">;
type G = IsArray<string>;
type I = DeepReadonlyArrayUnsafe<string[]>;

I also try to break the bits I'm working on down into as many smaller declarations as possible so I can get in there and test bits individually. Then of course there are those few times when breaking them up (buggily?) changes the result like I hit earlier with this... That's no fun.

@SimonMeskens
Copy link

@TheOtherSamP Do you know about these two issues?

#17455
#17521

It sounds like you're running into these bugs maybe.

@SamPruden
Copy link

Oh, good find @SimonMeskens! It's hard to know if those are the exact issues I'm running into, but they're (the second particularly) definitely highlighting problems in the same area I'm dealing with, looks like a good fit.

I'm a little worried that the recursive stuff in my DeepReadonly<T> is going to break when those get fixed. I'm not really doing anything too hacky in the recursion itself though, so if it does break I think it means we may have a further problem with how recursion is currently designed. Although actually... I think that would be fixed by #17636.

@SamPruden
Copy link

I stuck my gist into the playground with a little test right at the bottom, if anyone wants to play with DeepReadonly<T> as it stands it's here.

@KiaraGrouwstra
Copy link
Contributor Author

types C ~ I

@TheOtherSamP: yeah, my gist got full of things like that until I recently turned them into 'tests' to publish as repo.

@SamPruden
Copy link

@tycho01: Smart. Yeah, at this rate I'm having enough fun playing about that I might start putting together my own rival type library like yours. Not actually to compete, but it might be interesting to see if there are things we come up with different approaches to.

I also have the sudden urge to try to leverage the compiler into integrating type based tests into existing testing tools... No idea if that's even possible, and it's probably insane, but it might be fun. I'll add it to my list.

@KiaraGrouwstra
Copy link
Contributor Author

@TheOtherSamP: feel free to check how I'm doing the tests now, basically expressions asserting type outcomes that'll give compiler errors if they don't match (<=). So I 'run tests' just by compiling spec files. I just now ran that through tee to add error output to version control as well. That way Git GUIs will just visually show changes in errors.

@SamPruden
Copy link

@tycho01 That's way more sane that what I'm probably going to waste a load of time trying before finally admitting that your way was best all along.

@gcanti
Copy link

gcanti commented Aug 7, 2017

@TheOtherSamP @tycho01 I'm using typings-checker, you can define positive and negative assertions about TypeScript types and errors https://github.com/gcanti/typelevel-ts/blob/adfce078f96d0491dc5736a23a459330f2e8d579/typings-checker/index.ts#L87

@SamPruden
Copy link

@gcanti Oh wow, that looks fantastic, thanks for that link! Maybe not quite suited to the rapid iteration during prototyping, but great for marking things down once we've got them wrangled a bit. That was basically what I was going to try to make anyway, so that's saved me a lot of wasted time. 😅

@KiaraGrouwstra
Copy link
Contributor Author

@TheOtherSamP:

I loved PlayGround as well, and regressions weren't much of an initial concern; I was just forced to convert it as eventually a few types snuck in that had terrible performance / did not terminate.
I wanted to just comment half the types to find them binary search style, but the types depended on each other in many directions and finding the culprit was terrible.
My current situation slightly improved over that by allowing an attempt to compile one file (+ all deps) at a time.

@gcanti:

Fair enough, that does look like a potential improvement in the face of expected errors. On actual failed expectations, I recall I had a fork of it ditching line numbers from output so diff logs wouldn't get noise from line number changes, might have use for that here too. Guess I specialize in libraries that error.

I hated having to give every test line a name btw 😅, fortunately that's separate.

Seems unlike me you're also using value level expressions in your tests. That's interesting to me; I'd kept it type-level only. I don't even have any real considerations there, just sorta happened.

@jcalz
Copy link
Contributor

jcalz commented Oct 6, 2017

@tycho01 When you say

  • keyof: create a union of string literals from a type's keys. warts:
    • indices (whether number or string) get ignored

What do you mean by "ignored"? Note that keyof { foo: number; [k: string]: any } returns string, not "foo". Is that what you mean (I would call this the opposite of being ignored, myself) or did this behavior change? I do wish there were a way to get just the non-index keys from a type... is that a type operator I missed from your list?

@KiaraGrouwstra
Copy link
Contributor Author

KiaraGrouwstra commented Oct 6, 2017

@jcalz: sorry, lemme fix that, looks like I was mistaken there!
That actually seems a bit unfortunate though, guess that means it's hard to operate on constructs like that in such a way as to retain the key-specific info...

or did this behavior change?

Could be, but for all I know I just messed up there somehow. :)

I do wish there were a way to get just the non-index keys from a type... is that a type operator I missed from your list?

Hm.... before I thought that e.g. Omit would lose the indices while retaining the keys. In that case that might have been a way. Otherwise, it's gonna be tough.

This did just helped me think of an implementation for ObjectHasStringIndex though!

@KiaraGrouwstra
Copy link
Contributor Author

Marking as closed since a discussion doesn't require moderator attention.

@KiaraGrouwstra
Copy link
Contributor Author

KiaraGrouwstra commented Feb 15, 2018

@jcalz @MartinJohns here is fine to me.
I tried for a bit to see if I could strip the index off an object type, but haven't managed yet.
on a related note, ObjectHasStringIndex works, but isn't helping here.

I guess if we had a keyof equivalent not screwed up by the string index, that should get us there. It seems pretty tough though. So the real keyof would yield e.g. "a" | string, which just simplifies to string, removing all the info we were actually interested in.

So the more realistic approach seems to be to use a filtered map instead. My attempt has been among the following lines:

type T = { a: 1, [k: string]: number };
type stripped = { [P in keyof T]: string extends P ? never : T[P] }; // want { a: 1 }, got { [x: string]: string }

Not sure why it fails. :(

@jcalz
Copy link
Contributor

jcalz commented Feb 15, 2018

Yeah that's about as far as I got too.

@KiaraGrouwstra
Copy link
Contributor Author

@jcalz @MartinJohns in retrospect, guess it fails because it still relies on keyof... which means back to square 1. :(

@jcalz
Copy link
Contributor

jcalz commented Apr 23, 2018

Looks like IsUnionType<T> can now be implemented using distributive conditional types, even for T that do not extend string:

type IsUnionType<T, Y=true, N=false> =
  [T] extends [infer U] ? U extends any ? [T] extends [U] ? N : Y : never : never

This exposes some fun details of what the compiler considers a union:

const literalsGetAbsorbed : IsUnionType<string | 'a'> = false;
const booleansGetDistributed: IsUnionType<boolean> = true;
const intersectionsOfUnionsAreReduced: IsUnionType<{a: 0} & ({b: 1} | {c: 2})> = true;

@SimonMeskens
Copy link

@tycho01 Are you still looking for a way to strip indexes? I have a way to do it

@SimonMeskens
Copy link

Actually, my way of doing it corresponds to your last attempt up above, which now seems to work, so you should be golden.

@jcalz
Copy link
Contributor

jcalz commented Apr 24, 2018

Doesn't that still give you type stripped = { [x: string]: never; a: 1 } instead of the desired { a: 1 }? How do you use that to strip the index?

@SimonMeskens
Copy link

Yeah, totally my bad. I thought I had discovered some new trick, when I didn't. I figured out most of the issues people were having as I bashed my head against it for an hour yesterday.

@microsoft microsoft locked and limited conversation to collaborators Jul 31, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Discussion Issues which may not have code impact
Projects
None yet
Development

No branches or pull requests

7 participants