-
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
Design Meeting Notes, 9/25/2020 #40779
Comments
Code sample from @ahejlsberg
|
One outer commentary to help the mental model on this one - for any type
Again, this is just a concept. Not committed, syntax not tied down. |
Conditional assignability can be already used for function parameters case. declare enum digit { digit = '' }
type Digits = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
type Digit<D> = D extends Digits ? D : digit
declare function doSomethingWithDigit<D>(digit: Digit<D>): any
// OK
const a = doSomethingWithDigit('1')
// Argument of type '"a"' is not assignable to parameter of type 'digit'.ts(2345)
const b = doSomethingWithDigit('a') It will be great to see this feature on the type level. But we also need to be able to handle cases when our type depends on another ones. So I suggest make this conditional types generic. type LensKey<O extends Object> = match S extends string =>
// Traversal case
O extends ReadonlyArray<any> ? S extends '[]' ? true
: S extends keyof O ? true : false
declare function getLens<O extends Object, K extends LensKey<O>> (obj: O, key: K): any |
Would conditional assignability be a better place to surface 'throw types'? The conditional assignability concept directly hooks into the definitions of type relations, so would this be a better place to add helpful information? Every match type can have some doc string that is optionally used by the type-checker when the match expression evaluations to false. For example: type NonZero = match N extends number => [N] extends [0] ? false : true
reason `Non-zero number expected.`;
function checkedDivide(x: NonZero): number {
if (x === 0) throw new Error('')
return 5 / x
}
checkedDivide(0) // error. 0 is not assignable to NonZero: Non-zero number expected.
type Uncallable = match N extends never => false
reason `This function should never be called`
function dontCallMe(arg: Uncallable) { } As an aside. What happens in this case? type Weird = T extends never => false;
const x: Weird = undefined as any as Weird; |
Looks like conditional assignability (with somewhat extended functionality) can make #17428 irrelevant, while also providing a way to partially (or even completely) solve #14400, which is very good news: // Return the type to check against instead of basic true/false validation
type Match<T> = match S => S extends T ? S : T;
declare const f: <U>(input: Match<U>) => typeof input /* should preserve original matched type */;
interface Foo {
bar: 'baz';
}
const obj = {
test: 42
};
// Validate obj by a generic type
const r = f<Foo>({
bar: 'baz',
...obj,
});
/*
r is of type
{
bar: 'baz',
test: 42
}
*/
const err = f<Foo>({
// Error here?
baz: 'bar'
}); So, instead of the resulting type of conditional assignability to tell whether the type is assignable or not, it seems to me as a lot more practical and powerful to make the resulting type the one to check against. This way, previous examples can be rewritten in the following fashion: type Digit = match S extends string =>
S extends '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
// Return the matched type itself, as it already meets our requirements
? S
// Return `never` to trigger impossibility of assigning `S` to `Digit`
: never;
type Digits = match S extends string =>
S extends `${Digit}${infer R}` ? R extends '' | Digits ? S : never : never;
type Decimal = match S extends string =>
S extends `${'' | '+' | '-'}${Digits}` ? S
S extends `${'' | '+' | '-'}${Digits}.${Digits}` ? S :
never;
type Extent = match S extends string =>
S extends `${Decimal}${'px' | 'pt'}` ? S : never; This allows to not only forbid or allow assignability, but also restrict it more easily without multiple deeply branching conditionals, while also keeping the declaration much more readable: // This contraption is not intuitive nor understandable
///unless you know beforehand that it somehow validates
// the type based on whether the conditional returned true or false,
// as no types in TS currently work this way
type Digit = match S extends string =>
S extends '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' ? true : false;
// This, however, aligns perfectly with how types currently work from the user's perspective,
// and will probably raise less questions about what this means exactly, allowing easy adoption
type Digit = match S extends string =>
S extends '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' ? S : never;
declare function f(d: Digit): unknown; |
@Raiondesu this behaviour can be already done using plain TypeScript. type Cast<X, Y> = X extends Y ? X : Y
declare const getValidator: <V>() => <U>(input: Cast<U, V>) => typeof input
interface Foo {
bar: 'baz'
}
const validateFoo = getValidator<Foo>()
// OK
validateFoo({
bar: 'baz',
test: 1
})
// Error
validateFoo({
baz: 'bar'
}) And you also can use
But the main problem of such approach is parameters inference using // [digit: digit] or [digit: never] if you used never type
type P = Parameters<typeof doSomethingWithDigit> Type It can be solved by your suggested |
@awerlogus, the behaviour you described in the first example is exactly what bothers people in #14400 (see last few comments there). This is undesirable boilerplate code that mangles libraries' APIs, and people rightfully complain about it. I agree, however, with defining the core problem as a lack of HOTs (as more and more workarounds for them seem to pop-up in TS over time). The core problem I was trying to solve there was TSs inability to simply validate (not restrict!) and forward type information without capturing it in some sort of a generic parameter, which is, I think, the whole reason behind #17428 ("if TS can only do that using generics, let's make everything generic!"). I do understand I might be overthinking this, but after all, if conditionally assignable types "already" capture the matched type (somehow, outside of generics) to validate it - why refuse to use it to the end anyway? |
While I really like the concept of the conditional assignability, I think a major issue is how you match one against the other; i.e.: type Digit = match S extends string =>
S extends "0" | "1" | "2" | "3" | 4" | "5" | "6" | "7" | "8" | "9" ? true : false
type Bit = match S extends string =>
S extends "0" | "1" ? true : false
type Test = Bit extends Digit ? true : false; |
Unrelatedly, this would give us many advanced types: type Complement<T> = match U extends any => U extends T ? false : true;
type StrictSubtype<T> = match U extends T => T extends U ? false : true;
type NonUnion<T = any> = match U extends T => IsUnion<U> extends true ? false : true;
type UniqueSymbol = StrictSubtype<symbol> & NonUnion<symbol>;
// Utility, works sans this change
type IsUnion<T> = (T extends any ? (x: (x: T) => T) => any : never) extends (x: infer U) => any ? U extends (x: any) => infer V ? [T] extends [V] ? true : false : false : false; |
Looking at the digits example from Anders' snippet.
And it leaves me thinking... Without this syntax, users might solve this problem as follows.
type AreDigits<S extends string> = S extends `${infer C0}${infer CRest}`
? C0 extends '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
? CRest extends ""
? true
: AreDigits<CRest>
: false
: false;
type IsFloat<S extends string> = S extends `${infer L}.${infer T}`
? AreDigits<L> extends true
? AreDigits<T> extends true
? true
: false
: false
: false;
// etc ... ... with this approach, we run into three issues:
I'm curious why the solution is new syntax, instead of solutions to those three sub-problems? For instance: type Src = "Hello 254.931";
type FloatPlaceholder = _<IsFloat>;
type MatchResult = Src extends `${infer L}${FloatPlaceholder}`
? Src extends `${L}${infer Float}`
? Float
: never;
: never;
declare const matchResult: MatchResult; // `"254.931"` Here, the underscore is an intrinsic utility type, which accepts a boolean-returning generic type. The result can then be used within a pattern. Seems this approach might take less effort to grok. Then again, I'd imagine there are performance considerations of which this is lacking. Still, I'd love to hear about why conditional assignability is the preferred route! |
@harrysolovay ATM, they don't have any official HKT support (though I would love for them to add this). I think the approach they're considering taking is basically the same as yours, but with a syntactical approach rather than a utility type approach. With the syntactic appraoch, you could write While I definitely think they should add HKTs (which could "easily" be accomplished with a Also, is there another issue for the conditional assignability that should be commented upon, rather than cluttering up these meeting notes? |
Not sure if this conversation calls for another issue, or if it should exist in Discord, or when / if GitHub discussions will be enabled for this repo. Hopefully a TS team member can provide guidance as to what is the correct forum for this discussion. Until then, I doubt we're imposing by commenting here (although sorry if we are!). While fully-fledged HKTs would be nice, that's not what I'm suggesting. Yes, it's similar in the sense that there's some deferred evaluation of generic types. Yet there's a more-specific way to satisfy this use case: the ability to turn a string-accepting, boolean-producing generic type into a pattern placeholder. Even if we had this ability, conditional assignability may be more ergonomic and preferable. |
I don't think "deferred evaluation of generic types" actually has any meaning in typescript as it stands; you can't use generic types as types without supplying the type parameters.
I don't think these were supposed to be limited to strings; I think that the use cases are just more obvious with strings. Regardless, I think it would be a significant anti-pattern to have HKT-esque support for one specific built in utility type (or HKT support only for returning booleans). If they are resistant to adding general HKT support, it can be syntactic (as they've been discussing), similar to how they've implemented e.g. object maps. I think I may be misunderstanding what you are suggesting; are you suggesting an alternative to conditional assignability, or an alternate syntax for it? |
@tjjfvi thank you for pointing out that this is not limited to string literals. I'm suggesting new syntax may not be necessary. type Digit = match S extends string =>
S extends '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' ? true : false; vs. type IsDigit<S extends string> =
S extends '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' ? true : false;
type Digit = ConditionalAssignability<IsDigit>; Users are already familiar with generic and conditional types. I'm curious why introducing new syntax is necessary? Why not just introduce an intrinsic utility type for turning a predicate such as On another note, I'd like to know more about what this means?:
Does this mean that one cannot pass a conditional assignability to a generic type? Aka., is the following invalid?: type Digit = match S extends string =>
S extends '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' ? true : false;
type MeetsConstraint<E, C> = E extends C ? "The constraint `C` is met!" : "No!";
type Result = MeetsConstraint<"1", Digit>; // expecting "The constraint `C` is met!" If so, that would be unfortunate. |
Another random thought on this proposal: AFACT, the way this is written, conditionally assignable types could violate the "axiom" that if type IsUnion<T> = (T extends any ? (x: (x: T) => T) => any : never) extends (x: infer U) => any ? U extends (x: any) => infer V ? [T] extends [V] ? true : false : false : false;
type NonUnion<T = any> = match U extends T => IsUnion<U> extends true ? false : true;
type TestAssignablility<A, B> = A extends B ? true : false;
// Equivalent to `[true, true, false]`?
type Test = [TestAssignability<"A", NonUnion>, TestAssignability<"B", NonUnion>, TestAssignability<"A" | "B", NonUnion>]; Do any other types currently behave this way? |
@DanielRosenwasser I'm sad to see that this issue has been closed. I don't mean to impose by asking, but I'm curious if/why the team isn't pursuing conditional assignability. To me, it seemed like an extraordinary feature that would open countless doors to future DXs. |
@harrysolovay DanielRosenwasser just closed 49 design note issues, so I doubt it was anything specific. |
Yeah, but also is this what you're looking for? #30639 |
@DanielRosenwasser No, I think they were referring to the "Conditional Assignability" mentioned in the design notes
|
Is the "conditional assignability" concept being tracked anywhere? Still have my fingers crossed that we can get some of TypeScript's inference capabilities in assignment operations. |
Object Spreads and Union Types
#40754
#40755
One set of users: have been working with long builds, throw 8GB at it
They have a huge object type written like
This ends up creating a MASSIVE union type based on making a conditional property.
The users don't want a union, they just want a big old blob of optional properties.
We could just "recognize the pattern" and provide a "weaker" type (i.e. one type with lots of optionals instead of a union where every other union member makes the property
undefined
).In 3.8, we added this logic, but only for single-property objects. We were afraid to do this too broadly.
Other workarounds?
as CSSProperties
Can we leverage a contextual type?
Some feel like we should just always do this.
One option might be to provide a quick fix to write the all-optional property object type when the expression is too complex.
Sounds like we always want to use the all-optional properties, but can't do that for 4.1.
throw
Types#40468
throw
type "ever comes to pass", it becomes an error.never
s, ornever
s with a reason."currentNode
, we've had that for a while, works decently well.never
in the tail, you end up with more types.never
, especially because of unioning semantics.never
typeany
never
" and signaling "anti-any
".Conditional Assignability
[[Look how our type helpers look like parser combinators now.]]
But you can only trigger the inference on these type aliases for literals by writing them in call positions.
What if you could?
Mental model:
Foo
to aDigit
,S
is bound toFoo
, and is then related to that.string
) and the type then evaluates totrue
.Out of time.
The text was updated successfully, but these errors were encountered: