-
Notifications
You must be signed in to change notification settings - Fork 229
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
All I Want for Christmas Is… These Seven TypeScript Improvements #24
Comments
Accessing types at runtime will never ever happen as it is an explicit non-goal. The various libraries that attempt to tackle it do a pretty good job. I think tacking into the compiler would be a mistake. The modularity is good. 100% agree on what you call “evolving” types. It’s something OCaml, Flow, and ReScript do very well. Right now, TS only infers types in one direction. It would be pretty helpful if it could infer types from usage not just for the simple cases (n in n++ is a number) but also for functions that curry complex generics. Right now the only way to do it is to copypaste the whole type signature and tweak it. It’s very burdensome, just take a look at react-query-toolkit code. Another wish that I could tack on is OCaml-like variants. I don’t just want a “number” type, I want to be able to declare a Milliseconds(number) and a Seconds(number) which should not be interchangeably used without going through an appropriate conversion. Flow had a lot of this, but lost out because instead of trying to help describe real world JS, it attempted to prescribe a different (safer) style of JS which developers largely rejected for being a pain. The ensuing lack of library support effectively killed it. |
For me a great present from the typescript team would be finally getting some action on this issue that a lot of people desperately want. It would allow type access at compile time, and a lot of those libraries would be able to generate code at compile time, based on types, including potentially generating some kind of RTTI style code. |
outdated (read comments bellow) I hope this will not give an error in the future: const logLevels = ["info", "warn", "error"] as const;
type LogLevel = typeof logLevels[number];
const isLogLevel = (unknown: unknown): unknown is LogLevel =>
logLevels.includes(unknown);
// ^? Argument of type 'unknown' is not assignable to parameter of type '"info" | "warn" | "error"' (also would be interesting to learn if there is a good reason for this to be an error) |
@shamsartem its an error because much like The following doesn't narrow types as expected either:
|
@vezaynk I don't even expect it to perform type narrowing. I just expect it to accept any arguments that I through at it. The "type narrowing" part is done manually using Everything will work if you do it like this: |
@shamsartem Sorry if the example seemed unrelated. The issue is in fact because of the lack of type narrowing. The type signature of
What you're wishing is for it to have an overload that looks something like this:
The trap in this scenario though, is that this becomes legal with no warning from ts:
Pardon any errors, Im typing this on my phone. |
@vezaynk for me it would be completely fine if |
While it would solve the issue for If I write
TS flags an error that makes my code incorrect. And makes me fix it:
The overload would prevent TS from warning me about this incorrect code. |
@vezaynk Good point. That seems to be the actual reason it is done like that. Thanks! |
Interesting example @shamsartem. I agree with everything @vezaynk said. There are many patterns that "work" in JavaScript that TypeScript chooses to disallow. This is ultimately a judgment call made by the TS team, and in your case the error was a false positive. But there are many other cases where this would catch real errors. You can't have it both ways. I'm not offended at all by a type assertion in this case ( const isLogLevel = (val: any): val is LogLevel => logLevels.includes(val); Another option would be to make your own version of const looseIncludes = (xs: unknown[], x: unknown) => xs.includes(x);
const isLogLevel = (unknown: unknown): unknown is LogLevel =>
looseIncludes(logLevels, unknown); // OK |
This is what Anders calls "spooky action at a distance" (I believe this is a quote from one of his tsconf keynotes, not sure which year). It is certainly convenient in some cases, though from my brief experience with Flow, I remember that it can make it much, much harder to figure out where the real error is (it's not always where the error is surfaced). "Evolving" types are much narrower in scope.
There are some patterns around this, though I certainly agree that they can be awkward in practice: https://basarat.gitbook.io/typescript/main-1/nominaltyping |
For what it's worth, in the example you give, you can have optional generics by declaring a default value:
This works: function makeLookup<T, K extends keyof T = keyof T>(k: K): (obj: T) => T[K] {
return (obj: T) => obj[k];
}
interface Student {
name: string;
age: number;
}
const lookupName = makeLookup<Student>('name');
const lookupAge = makeLookup<Student>('age');
console.log(lookupName({"name": "Alice", "age": 23}));
console.log(lookupAge({"name": "Bob", "age": 21})); |
@SamirTalwar depends what you mean by "works" :) Your code does type check and it does what you expect at runtime. But the types aren't are precise as you'd like: // ...
const alice = lookupName({"name": "Alice", "age": 23});
// ^? const alice: string | number
const age = lookupAge({"name": "Bob", "age": 21});
// ^? const age: string | number Ideally these would be The issue with default values for type parameters is that |
@danvk: Thanks for the clarification! Now I get it. |
For what it's worth, you can actually implement a function makeLookup<K extends string>(k: K) {
return function lookup<T extends { [key in K]: any }>(obj: T) {
return obj[k];
}
}
const lookupName = makeLookup('name');
const lookupAge = makeLookup('age');
const alice = lookupName({"name": "Alice", "age": 23});
// ^? const alice: string
const age = lookupAge({"name": "Bob", "age": 21});
// ^? const age: number |
And if you want explicitly bind to it to function makeLookup<E>() {
return function <K extends string>(k: K) {
return function lookup<T extends { [key in K]: any } & E>(obj: T) {
return obj[k];
}
}
}
interface Student {
name: string;
age: number;
}
const lookupName = makeLookup<Student>()('name');
const lookupAge = makeLookup<Student>()('age');
const alice = lookupName({"name": "Alice", "age": 23});
// ^? const alice: string
const age = lookupAge({"name": "Bob", "age": 21});
// ^? const age: number
const missingAge = lookupName({"name": "Bob" }); // Argument of type '{ name: string; }' is not assignable to parameter of type '{ name: any; } & Student' The trick to working around optional generics is by passing in one at a time. In this case we pass in Student separately from the rest. Not saying a real optional generics implementaiton wouldn't be nice, but it is workable. |
@vezaynk Yes, currying is the standard workaround for this issue. You can also use a class, see my blog post from 2020. |
All I Want for Christmas Is… These Seven TypeScript Improvements
Effective TypeScript: All I Want for Christmas Is… These Seven TypeScript Improvements
https://effectivetypescript.com/2022/12/25/christmas/
The text was updated successfully, but these errors were encountered: