Closed MeirionHughes closed 4 years ago
Another relevant example is bluebird's promisifyAll. E.g.:
type Before = {
foo: (callback: (err, res) => void) => void
bar: (x: string, callback: (err, res) => void) => void
}
// promisifyAll would map Before to After - note all keys have 'Async' appended:
type After = {
fooAsync: () => Promise<any>;
barAsync: (x: string) => Promise<any>;
}
let before: Before;
let after: After = promisifyAll(before);
I think unusual complex type operations like this should be supported by programmatic type builder like #9883.
Digging this one out since I'm wrestling with a use case for this right now: Scoping redux actions.
I have an interface Action
:
interface Action<Type extends string> {
type: Type
}
to avoid clashes with (e.g.) third party libraries, I scope my actions, for example like this:
export const createScopedActionType = _.curry((scope: string, type: string) => `${scope}/${type}`);
const createActionInMyScope = createScopedActionType("MyScope");
const MY_ACTION = createActionInMyScope("MY_ACTION");
It is known at compile time that MY_ACTION
will have a value of "MyScope/MY_ACTION"
and such should be its type. Typescript however sees it as a string.
I could obviously assign the resulting value directly, but that would reduce maintainability since with the approach depicted above I can simply reuse my partially applied function. So I only have to define my scope once. If I were to use regular assignments, I would have to change multiple lines of code (and be in danger of missing an instance!) if the name of the scope would change.
A compile time evaluation of functions called with literals (if evaluatable, as in: no side effects, no parameter modification => pure functions) should yield the final type/value returned by the function call.
C++ has a similar concept with constant expressions (constexpr
).
I'm not sure in how far Typescript would allow the implementation of such a feature, but it would be a great help in keeping code maintainable and modular.
It might be nice to revisit this issue in light of the 2.8 enhancements. Specifically, I think this issue is the only thing prevent a good implementation of Bluebird.promisifyAll
. It was mentioned previously in this thread, but I think return type inference was an important piece of the puzzle that was also missing, until now.
In our library, a Component
class has for each of it's private properties (here id
) an associated public getter function:
class Component {
private _id: string;
public get_id() {
return this._id;
}
}
For each component we have an additional Properties
interface that - in this case - has an id
property:
interface Properties {
id:? string;
}
There is a lot of error-prone redundancy in here which caused us a lot of trouble in the past. With the new conditional types feature of TypeScript 2.8 I would love to see a linq-style syntax in indexed types:
type Properties<T extends Component> =
{
[ P in keyof T where P.startsWith( 'get_' ) select P.replace( 'get_', '' ) as K ]: ReturnType<T[K]>;
} |
ReturnType is defined in TypeScript 2.8
let componentProperties: Properties<Component>;
componentProperties.id = "foo"; // string is fine
componentProperties.id = true; // error
In Loopback, a DB's order clause is specified as:
{ order: 'field ASC' }
or
{ order: [ 'field1 DESC', 'field2 ASC' ] }
I think this feature is required for TS to type this field appropriately.
This is very important for things like using Bluebird.js to promisify everything in a module, as @yortus and @Retsam mentioned previously to this.
Using keyof
, it is now possible to transform the types of a module such as import fs from 'fs'
which is passed to Promise.promisifyAll(...)
. Which means this can almost be typed automatically. (And I strongly disagree with @saschanaz, code generation is not the right tool for this job. We can do it automatically!)
The only thing missing for this to work is the feature requested by this issue: support [expression] + [string literal]
expression in types. I don't think it would be too hard, and I would even be glad to start on it and make a pull request, if someone could point me in the right direction of where to get started!
@sb-js Looks like this would be a good place to start, as you'd need to add a bit that would parse a plus sign after a mapped type parameter.
@drsirmrpresidentfathercharles Thanks for the pointer. I will give it a shot this week.
another case:
interface IActionsA {
update: number;
clear: number;
}
interface IActionsB {
update: number;
clear: number;
}
interface INamespaceMap {
'moduleA/': IActionsA;
'moduleA/moduleB/': IActionsB;
}
// No matter how to write it
type ConnectKey<N, T> = N + [ P in keyof T ];
interface Dispatch<Map> {
<N extends keyof Map, K extends keyof Map[N]>(type: ConnectKey<N, K>, value: Map[N][K]): void;
}
let dispatch!: Dispatch<INamespaceMap>;
dispatch('moduleA/update', 0);
dispatch('moduleA/moduleB/delete', 0);
This will bring great help to vuex.
I am writing a library that converts some values to observables:
inteface Foo {
bar: any
}
// the library should convert above interface to
inteface Foo2 {
bar$: any
}
// syntax like below would be cool
type Foo2 = {
[(K in keyof Foo) + '$']: Observable<Foo[K]>
}
// such expressions would be useful not only to mutate property names,
// but other strings in type system as well (this behavior is not desired as much, though):
type Foo = 'bar'
type Foo2 = Foo + 'Addition' // gives the string type "barAddition"
Another case.
In react-native-extended-stylesheet styles can hold media queries starting with @media ...
string. For example:
{
'@media ios': { ... },
'@media (min-width: 350) and (max-width: 500)': { ... }
}
Currently I need to manually enumerate all keys used in app:
type ExtraStyles = {
'@media ios'?: ExtendedStyle;
'@media (min-width: 350) and (max-width: 500)'?: ExtendedStyle;
...
}
It would be great to define universal type with keys matching regexp:
type MediaQueryStyles = {
[/@media .+/i]?: ExtendedStyle;
}
This would help a lot with supporting dot-syntax keys à la lodash.get
, I think.
Having Split<T extends string>
and Join<T extends string[]>
types would help, too.
Supporting dot syntax keys would be awesome. For example MongoDB queries are frequently written as {"obj.nested.key": "value"}
which is impossible to type right now.
Yet another case:
I was wondering what could be done in relation to pnp/pnpjs#199 in pnpjs. With pnpjs you can query collections of items in SharePoint and project only a subset of the properties of those items that you will need using the function select
. For example:
documents.select('Id', 'Title', 'CreatedBy').get();
The library also supports selecting expanded properties of complex types. In the above example, we would be able to get both the Id
and UserName
subproperties of the CreatedBy
property:
documents.select('Id', 'Title', 'CreatedBy/Id', 'CreatedBy/UserName').get();
It is currently impossible to type the select
arguments or its return type. The library currently casts everything to any
by default which is a shame since almost all usage scenarios of this library use these operations.
Any more info on if this will be a possibility? Dot notation mongo queries are one of the last things missing typing in my project =)
Well, it doesn't have any tests or anything, but I do have a working implementation at my fork. This is my first time ever contributing to TypeScript, so some comments would be great as I work towards a possible pull request.
@ahejlsberg any thoughts on this?
Typescript must not invent new keywords and type level symbols every now and then. Typescript can introduce built-in generic types for such things e.g.
type StringConcat<A, B> = /* built-in, resolves to `${A}${B}` when used */
type NumberAdd<A, B> = /* built-in, resolves to A+B when used */
const s: StringConcat<'foo', 'bar'> = 'foobar' // pass
const s: StringConcat<'foo', 'bar'> = 'foo' + 'bar' // pass
const s: StringConcat<'foo', 'bar'> = 'foo' + 'quu' // fail
Similarly, number operators could be useful:
const n: NumberAdd<1, 2> = 3 // pass
const n: NumberAdd<1, 2> = 1 + 1 + 1 // pass
const n: NumberAdd<1, 2> = 1 + 1 + 2 // fail
@anilanar To make this work for a mapping situation, I'm assuming
StringConcat<'foo' | 'bar', 'quu'>
would resolve to
'fooquu' | 'barquu'
?
@anilanar I dig that idea; what does it look like in the object mapping example, like what's shown in this comment?
@charles-toller def’ly. StringConcat is distributive over |
and &
.
@mAAdhaTTah [K in StringConcat<keyof MyInterface, '$'>]
I think I'd prefer a syntax that allows us to take advantage of existing operations, rather than re-defining them individually. For example, StringConcat<T extends string, U extends string>
doesn't handle regex replacement -- if you want that you need StringReplace<T extends string, PATTERN extends string, REPLACEMENT extends string>
. Similarly, for number, needing to re-define addition, subtraction, multiplication, etc. etc. as generic types would be aesthetically unappealing, at least. Since const x: 'a' + 'b' = 'ab';
is currently a syntax error, that syntax seems both more concise and consistent to me.
Regarding syntax, I don’t think aesthetic elegancy should be the primary concern. Introducing operators that will be difficult to drop later is risky. Generic types are already part of the language.
NumberAdd
has some nice properties if TS had recursive types, you can define all number operators just usingNumberCompare
and NumberAdd
using conditional types and recursion.
I think TS should introduce orthogonal built-ins without filling TS with operators/keywords that feel like hacks rather than part of a well structured language.
With your proposal, +
is a binary operator alias for StringConcat
. If the community wants/needs type-level operator aliases or syntactic sugars for built-in generic types (or operator overloading, but for types), that’d be another proposal.
Nevertheless I’m not a programming language designer. I’d seek advise from experts and possibly look at other languages that have type-level programming (Haskell/Purescript has some, Scala too which is closer to TS)
One more use case for this feature: https://github.com/drcmda/react-three-fiber/issues/11
react-three-fiber
is a library that provides every class that THREE.js
exports as React JSX elements with the first letter lowercased (so that they are treated like "host" elements by React and forwarded to the reconciler).
react-three-fiber
does not maintain a list of what is exported, it resolves the exports in runtime.
If a literal constexpr
-like string transformation feature existed, the types to extend the JSX.IntrinsicElements
interface could be autogenerated based on THREE
exports and keyof
.
The above comments to this proposal focus on string concatenation, but it wouldn't be enough for the use case of react-three-fiber
.
@sompylasar
react-three-fiber does not maintain a list of what is exported, it resolves the exports in runtime.
Maybe you can auto-generate type definitions in postinstall
phase with a fallback to types that were generated with the last version of three.js during publishing?
I don't think TS will have a feature anytime soon that would solve your problem.
I'm trying to achieve exactly what @anurbol is implementing. His suggestion is great and pretty readable. String interpolation could be used, too:
type Observablify<T> = {
[`${P in keyof T}$`]: Observable<T[P]>;
}
Personally, a syntax similar to Python's list comprehension feels more "correct", and I like the thought of such syntax:
type Observablify<T> = {
[(P + '$') for P in keyof T]: Observable<T[P]>;
}
I've waited for this, too! Would be a killer-feature but I've given up the hope...
Essentially any generic factory creating redux actions, whether that be the scoping example given above, the common request/success/failure pattern, or any other, is very difficult to type without this feature.
And without that, redux is hard to type, period. Not unless you are okay with writing tons of unnecessary boilerplate, simply because you cannot genericize a wide range of patterns.
@grapereader It’s redux’s “fault” in a sense to convey so much information in strings. Use tagged unions to handle stuff like fetch-start, fetch-success and fetch-failure.
type StartAction<A> = { type: 'fetch', phase: 'start' payload: A };
type SuccessAction<B>= { ... };
type FetchAction = StartAction | SuccessAction;
const handleFetch = <A, B>(action: FetchAction, handlers: { start: (action: StartAction<A>) => void, ... }): void;
You get the gist.
Are we really going to design TS based on patching design flaws of user-land code in JS?
Not that I’m against type-level functions to manipulate string literals. I just think TS can focus on some other areas right now.
While I really like the idea of a generic type that augments strings, as it feels much closer to how we also augment objects with mapping, StringConcat
has an obstacle on the way to implementation: it is a type reference that cannot be placed in any lib.d.ts
, which makes it unresolvable without a special case in the checker. The reason is that unlike many other TS builtins such as ThisType
or Omit
, it cannot be directly represented in TypeScript. Even types that have only meaning to the checker, such as ThisType
, have no problem being placed in the libs because they don't actually change the type of the object they're modifying. StringConcat
, on the other hand, directly modifies it's type arguments, and so the closest correct value we could mark as the type is string
, which is obviously undesirable.
Are we really going to design TS based on patching design flaws of user-land code in JS?
Yes. TypeScript's number one goal is to "Statically identify constructs that are likely to be errors." This issue is full of library maintainers that need this feature in order to meet that goal for their end-users.
Could improve typing for material-ui, example palette colors Example:
<Box color="primary.main">…
In my case, I'm interacting with an API that returns objects with PascalCased properties, and I'd simply like to generate a type that has camelCased properties. If the properties aren't 1:1 mappings, or if we need methods, we'll create classes. However if that's not the case, it would be nice to have a function that spits out the same object except with camelCased properties and have that typed.
Essentially something like :
export type CamelCased<T> = {[camelCase(K in keyof T)]: T[K]};
Voted. This would be incredibly useful for merging interfaces. My use case is a React component that wires two components together. I want the props to contain both component props and have a prefix to all keys that describes the context and helps avoid potential overlap conflicts. Right now I'm using a single object prop for the second component but this is not ideal from React best practices because prop change is referential.
I, too, would like to see this feature. In our codebase, we have configurable higher order components that allow customization of default properties on a wrapped object. In this case, when a property name is configured (e.g. prop
), we also configure, automatically, a 'default' version (e.g. defaultProp
). So, not only do we concatenate, but we also change the case of the property.
@kalbert312 Best practices only apply for simplistic use cases. You can overcome referential changes by introducing a HoC or a hook to manage this wiring of components and customize shouldComponentUpdate
to go one level deep during comprison (or useMemo
with hooks which is much better).
@webOS101 why not create a defaults
field into which you can put default values per prop?
Being able to type MongoDB queries would be a huge improvement on data consistency, I'd love to see this running in our Rocket.Chat code base.
I am trying to type an object with values that can either be accessed by obj[namespace][key]
or obj[namespace + "/" + key]
, but short of having a typeParam for namespacedkey and key, which have to be declared, I cannot see a way to type it. i.e. function f<Nk, K>(namespacedkey:Nk, key:K) ...
would have to be called as f("namespace/key", "key")
which allows users to accidentally use a different key for each approach.
This would definitely be crucial for my library's API.
There is a list of defined properties, and a list of dynamic suffixes. A developer may define their own suffixes, which should create valid pairs of: property name + suffix. Example:
Property | Suffix | Resolved string |
---|---|---|
margin |
md |
marginMd |
align |
tablet |
alignTablet |
justify |
anythingReally |
justifyAnythingReally |
It would be great to have this shiped, so the library could grant the best DX with accurate type definitions. Thanks.
This will help a ton when typing high order redux actions - making them totally type safe
My use case is pretty simple. We have a custom ORM-like solution. We have models that correspond with tables in the database. We allow the developer to construct a query, and the dev can define their own WHERE clause for the query using a JS object. I'd like to be able to do something like this:
export class Customer {
public id?: number;
public name?: string;
}
export interface WhereClause<T> {
[column: keyof T]: string|number|SomeAggregateObject;
}
(the value of the property might not match the type defined in the generic class)
And then my IDE would know what columns exist when I do something like this:
const where: WhereClause<Customer> = {
name: 'John Smith',
};
@HillTravis
export type WhereClause<T> = {
[TColumn in keyof T]: string|number|SomeAggregateObject;
}
you already can do that.
@weswigham I'm trying to implement what you suggested, but I'm still getting an error, specifically when the Customer
class has a public method.
As an example:
export class Customer {
public id?: number;
public name?: string;
public getName(): string {
return this.name;
}
}
export type WhereClause<T> = {
[TColumn in keyof T]: string|number|SomeAggregateObject;
};
const where: WhereClause<Customer> = {
name: 'John Smith',
};
In the above scenario, I get an error that the property getName
is required by WhereClause<Customer>
.
EDIT: Figured it out:
export type WhereClause<T> = {
[TColumn in keyof Partial<T>]: string|number|SomeAggregateObject;
}
Thanks for the help!
You should probably write
export type WhereClause<T> = {
[TColumn in keyof T]?: string|number|SomeAggregateObject;
}
instead - the ?
modifier on the mapping itself is much more idiomatic that relying on the modifier flowing in through the keyof T
target.
Another approach to this would be to have:
interface ICustomer {
public id?: number;
public name?: string;
}
Implement it on Customer and pass the ICustomer as T instead.
@akomm Thank you for your suggestion, but I prefer to avoid having two places to maintain the properties of the class. I can surely imagine someone in the future adding, removing, or modifying a property on the Customer
class and forgetting about the ICustomer
interface.
@HillTravis the other approach is convenient. However, your WhereClause will also allow a query on getName alongside name. I am not sure if that is desired. Or, and the circle closes then, you need Augment key during type mapping :). Sure, you can start hack around with conditional generics and mapping functions to never, or similar. But the solution is then not very fast and the error feedback not straightforward (getName signature
is not assignable to never).
Hi there 👋 I see there has been some discussion going on in this issue for quite some time now (3 years and counting! 🎂 ) and some suggestions and reasonable use cases have been thrown around. I also noticed that similar issues got pushed down in priority to the backlog.
I'd like to understand a bit how's the status on this. Are there any plans on tackling this feature on the foreseeable future? Thanks in advance 🙇
Hi, much love to typescript !
I myself would love this feature to get all possible paths in a POJO. Something like:
/**
*
* USELESS -- DONT USE !!
*
* Need to be able to transform Paths<T[K]>
* Something like: `${ K }.${ Paths<T[K]> }`
*
*/
type Paths<T extends object> = keyof T | {
[K in keyof T]: T[K] extends object ? Paths<T[K]> /** `${ K }.${ Paths<T[K]> }` */ : K
}[keyof T];
interface A { aA: number }
interface B { aB: A, bB: number }
interface C { aC: A, bC: B, cC: number }
/**
* Actualy resolve to
*/
type PathsA = Paths<A>; // 'aA';
type PathsB = Paths<B>; // 'aB' | 'bB' | 'aA';
type PathsC = Paths<C>; // 'aC' | 'bC' | 'cC' | 'aA' | 'aB' | 'bB';
/**
* Would be nice to have
*/
type NiceToHavePathsA = Paths<A>; // 'aA';
type NiceToHavePathsB = Paths<B>; // 'aB' | 'bB' | 'aB.aA';
type NiceToHavePathsC = Paths<C>; // 'aC' | 'bC' | 'cC' | 'aC.aA' | 'bC.aB' | 'bC.bB' | 'bC.aB.aA';
Cheers.
If anybody needs inspiration to work around this: my typed-knex library uses lambda's to kind of make this work. https://github.com/wwwouter/typed-knex
Aurelia follows the convention that for any field
foo
, whenfoo
is changed by the framework, then an attempt to callfooChanged()
is made. There is (seemingly) no way to describe this convention as a type (automatically) via the Mapped Type functionality alone.I would like to open discussion to the prospect of augmenting property keys during mapping.
For example: via arthimetic:
in this use-case specifically it probably would require https://github.com/Microsoft/TypeScript/issues/12424 too, but that is beside the point here.