Join GitHub today
GitHub is home to over 50 million developers working together to host and review code, manage projects, and build software together.
Sign upGitHub is where the world builds software
Millions of developers and companies build, ship, and maintain their software on GitHub — the largest and most advanced development platform in the world.
Throw types #40468
Throw types #40468
Conversation
|
Looking forward to trying out with the fix - not sure if I'm allowed to do this as a non-contributor but: @typescript-bot pack this. |
|
@typescript-bot pack this |
|
Hey @orta, I've packed this into an installable tgz. You can install it for testing by referencing it in your
and then running There is also a playground for this build. |
|
Idea: Find a way to "match" the error string into the typescript diagnostic object, to reuse the translation of diagnostic message. A possible solution:
That will match the Diagnostic.Did_you_mean_0 and therefore it will be automatically translated by the compiler. |
|
Idea: Find a way to emit other kinds of diagnostic, like suggestions or warning. If so, there should be another mechanism to offer a underlying type like
|
|
Oh I just realized we can throw a object type so it can include more details! Please help me to investigate the possibility of this to find a good shape of the error object. Possible properties:
|
| @@ -3032,6 +3032,10 @@ | |||
| "category": "Error", | |||
| "code": 2793 | |||
| }, | |||
| "Type instantiated results in a throw type saying: {0}": { | |||
dsherret
Sep 11, 2020
Contributor
Something shorter? Type instantiation threw. {0}?
Something shorter? Type instantiation threw. {0}?
|
Idea: Allow throw type to be handled by conditional type. This allows some error recovery or composing multiple errors. T extends throw infer E1 ?
U extends throw infer E2 ?
throw `\n ${E1}\n ${E2}`
: T : never |
import type { DiagnosticCategory } from 'typescript'
/**
* Since this is a type-level thing, any union in this type is considered invalid value
* cause their value is not determinate yet.
*
* `throw "string"` is convert to `throw { message: "string" }`
*/
type ErrorMessage = {
/**
* ! implemented !
* If diagnostic is not exist or not valid (key not found / not a tuple) this message will be used
*
* If it is not a string literal type, it will be formatted by `getTypeNameForErrorDisplay`
*/
message: any
/**
* What type should this throw type compatible(equal) with?
* Useful to "add" diagnostic message on a type and preserve itself
* @default never
*/
type?: any
/**
* ! implemented !
* TODO: provide completion for this in the language service
* Actually it is `[keyof Diagnostic (a @internal variable of ts), ...any[]]`
* @example ["_0_expected", T] results in "T expected" (with translation in different languages)
*/
diagnostic?: [type: string, ...args: any[]]
/**
* ! implemented !
* @default `Error` when undefined. `Message` when value is invalid.
*/
category?: 'suggestion' | 'error' | 'warning' | 'message'
/**
* It seems like deprecated is not in the DiagnosticCategory
*/
deprecated?: boolean
// ! Let developers custom the error code might not a good idea.
// code: number
/**
* When it happened on an identifer, replace the identifier with the suggestion
* e.g.: name => window.name
*
* When it happened on a type alias, do nothing
* When it happened on a CallExpression, do nothing
*
* ? Is this really useful cause the throw cannot get the original source text ?
*
* If we have higher kind types, this option can receive a un-instantiated generic type
* as a type-level function.
*
* @example
* type MyError<Context extends ...> = ...
* type T<U> = ... extends ... ? ... : throw {message: ..., suggestion: MyError}
*/
suggestion?: string
/**
* Let message be able to chain
*/
next?: ErrorMessage | ErrorMessage[]
} |
|
|
To be honest the initial idea is great - but adding diagnostics types and formatting feels like an overkill. It might even step into the linters world. The syntax is more complex too. I think having the ability to throw is already great as is. |
Hmm, I didn't introduce a new syntax for a "detailed" throw. It's a plain object literal type. I need some advice from the TypeScript team. If they think it's no need to do this, I'll stop working on that feature sets and focus on what will be accepted. Now I still have two complex ideas:
Is there anything I mentioned above that the TS team doesn't want? cc @RyanCavanaugh @orta @Kingwl |
|
Just as a data point - this PR makes my expect-type library for writing type-level assertions considerably better. The implementation no longer depends on rest-parameter hacks, and the error messages are much more useful. Re being able to use diagnostic objects, I'm inclined to agree with @Harpush - plain string messages being supported is a must-have, but supporting complex objects, probably needs more design and consideration, and are more of a nice-to-have. I'd expect trying to include them from the get-go will make it harder for this PR to be accepted - so purely selfishly, as someone who wants to use some form of this feature ASAP, my two cents: complex diagnostic object support should be split out into another PR. |
|
Thanks! I have already paused my work on the complex diagnostic messages and waiting for the TS team language meeting to decide. |
|
Should this work? type MustNumber<T> = T extends number ? T : throw `"Expected, but found "${typeof T}"`
type MustNumber2<T> = T extends number ? T : throw `"Expected, but found "${typeof T}"`
function foo<T>(x: MustNumber<T>, y: MustNumber2<T>) {
x = y; // error now
} |
|
hi, @jack-williams the playground has been outdated. The latest error message is on
|
|
Can somebody pack this again please? And also could it be possible that printing unions is not possible? |
|
Also I try to type a database migration using the following code: let a = {
posts: {
content: {
},
name: {}
}
}
let b = {
posts: {
content: {
},
namfe: {}
}
}
type SafeMerge<A, B> =
{
[K in Exclude<keyof A, keyof B>]: A[K]
}
&
{
[K in Exclude<keyof B, keyof A>]: B[K]
}
&
{
[K in keyof A & keyof B]:
keyof A[K] & keyof B[K] extends never ?
{
[K1 in keyof A[K]]: A[K][K1]
}
&
{
[K1 in keyof B[K]]: B[K][K1]
}
: throw `the following additions contain already existing columns: ${K}.${typeof B[K]}}`
}
let fdsfione: SafeMerge<typeof a, typeof b> = null as any
fdsfione.postsIn this case a nonlazy error would be useful. Do you have another solution and if not would you be interested in implementing this? Or do you think it's not worth it for such special usages? |
|
Hi @mohe2015, since this is a type-level operation, I think I have no idea about how to handle union cause it is representing "one of" those errors. Maybe intersections can be represented as multiple errors. Your latter example does work on the latest commit, error on
Oh do you mean it should error on |
|
@Jack-Works thanks for the fast reply. My initial idea was to do throw `the following additions contain already existing columns: ${K}.${keyof A[K] & keyof B[K]}`which would produce a really nice error message if only one column is duplicate. But it fails for multiple ones. The problem with the current solution (in my last comment) is that it also shows unrelated objects that aren't duplicates and also shows all children which are unrelated to the problem. Regarding the second part - yes I would love if it could error on |
|
@typescript-bot pack this |
|
Hey @orta, I've packed this into an installable tgz. You can install it for testing by referencing it in your
and then running There is also a playground for this build. |
|
@mohe2015 I have re-investigate your case, it is by design. Because your throw type is on the object property, the I have improved the intersection case. Any |
|
@Jack-Works In the case of this example playground,
my concern was not with the error message, but rather that I think it is wrong that two identical types are not assignable to each other. |
|
@jack-williams Now currently I didn't handle the assignability of throw types, which means they behave like |
|
Merged with master, due to #40580, now the |
|
Hi, I have read the meeting note! I have something to reply to and question about.
My early implementation does work on type instantiation, but I really found some problems (type get instantiation at the position I don't want to throw errors). The current implementation is case-by-case to raise errors on the evaluated type (for example, PropertyAccess, Identifier access, a function call, ...etc).
I don't understand what is "anti-`any`". But the type currently work in this way: it is printed as "never" (but actually not so it is not compatible with Question:
|
|
@typescript-bot pack this |
|
Heya @DanielRosenwasser, I've started to run the tarball bundle task on this PR at cdd5cd0. You can monitor the build here. |
This is close to what we meant by "anti-
We do like the idea about signaling a reason. It's kind of strange that this isn't localizable, but it's reasonable given that the code itself is written with some language in mind.
I'm not necessarily sure, do you mean being able to throw conditionals? Or support conditionals in a way that special-cases
I would hold off on any changes - we're not necessarily sure this is something we want in the language. If you're doing this mostly for the sake of experimentation, then it's really up to you. |
I'm meaning the ability of
|
|
Just wanted to share my temporary solution based on anti-any pattern: invalid.tsconst TypeErrorTag: unique symbol = Symbol('TypeErrorTag'); // don't export it!
export type invalid<Message> = {
[x in keyof Message | typeof TypeErrorTag]: x extends keyof Message
? Message[x & keyof Message]
: { readonly [TypeErrorTag]: unique symbol }; // anonymous unique symbol
};extend.tsHow to use it in hypothetical import { invalid } from './invalid';
export type Extend<
Base extends object | null,
Key extends string | number | symbol,
Value
> = { [K in Exclude<keyof Base, Key>]: Base[K] } &
{
[K in Key]: Value extends Base[Key & keyof Base]
? Value
: Base[Key & keyof Base] extends never
? Value
: invalid<{
message: 'Incorrectly overridden';
key: Key;
originalType: Base[Key & keyof Base];
newType: Value;
}>;
};
export function extend<
Base extends object | null,
Key extends string | number | symbol,
Value
>(base: Base, key: Key, value: Value): Extend<Base, Key, Value> {
return Object.create(base, {
[key]: { value, enumerable: true }
});
}UsageWhat gets a final user when uses a library written with help of import { extend } from './extend';
const a = extend(null, "x", 10); // typeof a === {x: number}
const b = extend(a, "y", true); // typeof b === {x: number, y: boolean}
const c = extend(b, "z", "Hello"); // typeof c === {x: number, y: boolean, z: string}
const d = extend(c, "y", false as const); // typeof d === {x: number, y: false, z: string }
const e = extend(d, "z", 123); // typeof e === {x: number, y: false, z: {
// [TypeErrorTag]: unique symbol;
// message: "Incorrectly overridden";
// key: "z";
// originalType: string;
// newType: number;
// }}
let x = e.z + 1; // in runtime x === 124
// yet in compile time it produces an error
let test: invalid<{}> = {} // {} !== invalid<{}>Forgery protectionUsers are unable to to access DrawbacksCompile time error occurs lazily only when corresponding invalid type is compared (and thus instantiated) with something else. So if nobody used that type value, it will not produce any errors. As it was mentioned it would be cool to have an early errors. Yet it exposes type instantiation time, that was previously hidden. ConclusionWhile still waiting for eager |






Fixes #23689
This PR introduces:
throw type_expr. Currentlythrowtype only throws when it is being instantiated.A new modifier to use in the template string(Removed due to #40580)typeof Tto make it easier to debug.Considered use cases:
Welcome to suggest more use cases!
Due to typeof modifier removed, some of the following examples are no longer working!
TypeAlias instantation
Prevent CallExpression
Prevent use of identifiers
TODO:
\${typeof T}``