microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.89k stars 12.47k forks source link

Augment Key during Type Mapping #12754

Closed MeirionHughes closed 4 years ago

MeirionHughes commented 7 years ago

Aurelia follows the convention that for any field foo, when foo is changed by the framework, then an attempt to call fooChanged() 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:

type Changed<T> = 
{          
   [ P in keyof T ] + "Changed" ?: Function;
}

class Foo{
  field:number;
}

let foo = <Foo & Changed<Foo>> new Foo();

foo.field = 10;

if(foo.fieldChanged)
  foo.fieldChanged();

in this use-case specifically it probably would require https://github.com/Microsoft/TypeScript/issues/12424 too, but that is beside the point here.

yortus commented 7 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);
saschanaz commented 7 years ago

I think unusual complex type operations like this should be supported by programmatic type builder like #9883.

Nimelrian commented 6 years ago

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.

Retsam commented 6 years ago

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.

markusmauch commented 6 years ago

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
ddurschlag6river commented 6 years ago

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.

ghost commented 6 years ago

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!

charles-toller commented 6 years ago

@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.

ghost commented 6 years ago

@drsirmrpresidentfathercharles Thanks for the pointer. I will give it a shot this week.

ZSkycat commented 6 years ago

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.

anurbol commented 6 years ago

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"
vitalets commented 6 years ago

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;
}
aleclarson commented 6 years ago

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.

evelant commented 6 years ago

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.

pedro-pedrosa commented 6 years ago

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.

evelant commented 5 years ago

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 =)

charles-toller commented 5 years ago

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.

rizqme commented 5 years ago

@ahejlsberg any thoughts on this?

anilanar commented 5 years ago

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
charles-toller commented 5 years ago

@anilanar To make this work for a mapping situation, I'm assuming

StringConcat<'foo' | 'bar', 'quu'>

would resolve to

'fooquu' | 'barquu'

?

mAAdhaTTah commented 5 years ago

@anilanar I dig that idea; what does it look like in the object mapping example, like what's shown in this comment?

anilanar commented 5 years ago

@charles-toller def’ly. StringConcat is distributive over | and &.

@mAAdhaTTah [K in StringConcat<keyof MyInterface, '$'>]

ddurschlag6river commented 5 years ago

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.

anilanar commented 5 years ago

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)

sompylasar commented 5 years ago

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.

anilanar commented 5 years ago

@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.

amitbeck commented 5 years ago

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]>;
}
markusmauch commented 5 years ago

I've waited for this, too! Would be a killer-feature but I've given up the hope...

grapereader commented 5 years ago

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.

anilanar commented 5 years ago

@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.

charles-toller commented 5 years ago

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.

akomm commented 5 years ago

Could improve typing for material-ui, example palette colors Example:

<Box color="primary.main">…
gmiklich commented 5 years ago

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]};

kalbert312 commented 5 years ago

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.

webOS101 commented 5 years ago

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.

anilanar commented 5 years ago

@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?

rodrigok commented 5 years ago

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.

Alpvax commented 5 years ago

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.

kettanaito commented 4 years ago

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.

Harpush commented 4 years ago

This will help a ton when typing high order redux actions - making them totally type safe

HillTravis commented 4 years ago

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',
};
weswigham commented 4 years ago

@HillTravis

export type WhereClause<T> = {
  [TColumn in keyof T]: string|number|SomeAggregateObject;
}

you already can do that.

HillTravis commented 4 years ago

@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!

weswigham commented 4 years ago

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.

akomm commented 4 years ago

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.

HillTravis commented 4 years ago

@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.

akomm commented 4 years ago

@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).

Yurickh commented 4 years ago

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 🙇

zto-sbenning commented 4 years ago

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.

wwwouter commented 4 years ago

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