Typescript: Augment Key during Type Mapping

Created on 8 Dec 2016  Ā·  88Comments  Ā·  Source: microsoft/TypeScript

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.

Literal Types Mapped Types In Discussion Suggestion

Most helpful comment

This suggestion is now implemented in #40336.

All 88 comments

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> {
}



md5-35a64b07076ca1f2908a04d038b70b0c



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

@wwwouter thanks for sharing, useful stuff there; unfortunately it still doesn't solve a very common need I've had which is to be able to concatenate string literals =]

Any update on this, guys?

Perhaps an even better question -- is there anything that members of the community who are particularly interested in this could do to assist?

Last activity I saw on this was on #36623, unclear if there is any plan to implement it or not. At least it is being discussed.

I need also to support dynamically created objects keys with dot notation.
Eg.

// transforming
type nested = {a: { b: number }}
// into
type flat = {'a.b': number}

The reason is, because Google Firebase SDK only allows updating nested objects via the dot notation (Firestore is similar to MongoDB).

But there's currently no way for me to type a payload after converting it into the dot notation.

@mesqueeb You can create an abstraction around firebase for type safety, e.g. create multiple overloads of function update<T, K1 extends keyof T, K2 extends keyof T[K]>(k1: K, k2: K2, val: T[K1][K2]): FirebaseUpdater => ({ [`${k1}.${k2}`]: val }) so thereā€™s a userland workaround with pretty good type safety.

@anilanar thanks for the suggestion!

I don't understand how I can use this though. I have a "flattenObject" that can take any object that a dev might pass, and then returns the "flattened" object with dot notification for props.

I'm able to use ts-toolkit to retrieve each "path" as an array of key-strings in TS, but I'm not able to recreate the type it returns; which is the object with flattened paths as dot notation keys.

@mesqueeb Basically you need to create your own types and functions and you must not rely on whatever types you are using for interacting with Firebase. That requires some advanced techniques such as creating Newtypes or Phantom types. Reference: https://dev.to/busypeoples/notes-on-typescript-phantom-types-kg9 or https://github.com/gcanti/newtype-ts

If you do the whole thing, just publish it open source as firebase-ts, perfectly type safe wrapper around firebase!

I never used firebase, I read the firebase API a little bit to give you an idea. I didn't fully understand how refs work, but with the following approach, everything is possible if you put some effort into it.

// This is incomplete, generics require some constraints
import * as TS from 'ts-toolbelt';

interface Database<T> {
  readonly _tag: unique symbol,
  readonly _type: T,
  readonly internal: firebase.database.Database,
}

interface Reference<T> {
  readonly _tag: unique symbol,
  readonly _type: T,
  readonly internal: firebase.database.Reference
}

export function database<T>(app: firebase.App): Database<T> {
  return unsafeCoerce({
    internal: firebase.database(app),
  });
}

export function reference<T, P>(database: Database<T>, path: P): Reference<TS.O.Path<T, P>> {
  return unsafeCoerce({
    internal: database.internal.ref(path.join('/')),
  });
}

export function update<T extends object>(
  ref: Reference<T>,
  values: Partial<T>,
): Promise<unknown> {
  return ref.internal.update(flattenObject(values));
}

function unsafeCoerce<A, B>(val: A): B {
  return val as unknown as B;
}
// Usage

interface MyFirebase {
  user: {
    id: string;
    name: string;
    company: {
      name: string;
    }
  }
}

const db = database<MyFirebase>(myApp);

// let's update company name.
update(reference(db, ['user', 'company']), { name: 'new company name' });

@anilar you proposal would work for a hardcoded number of levels. And in your last example you are passing the path to the reference, which would only allow you to change properties in the same subobject.

Let's say your data object is as follows:

interface User {
  homeAddress: {
    zipcode: string;
    valid: boolean;
  }
  workAddress: {
    zipcode: string;
    valid: boolean;
  }
}

And you only want to update the validity of the addresses, which in firestore (firebase is similar) is done as follows

db.collection('users').doc(userId).update({'homeAddress.valid': false, 'workAddress.valid': false})

How would you write a TS wrapper for that case?

@sk-

Thatā€™s because I thought firebase allows updates only for 1 path.

Another possibility is deep partial updates.

update(..., {
  homeAddress: {
    valid: false,
  },
  workAddress: {
     valid: false,
  }
}

Both APIs can co-exist.

Tell me 1 language that has type safety for concatenated strings (maybe except for compile level macros in cpp/rust; even then, I doubt they can split/parse strings in macros).

@sk- It works for infinite number of levels, TS has recursive types. You can look up typesafe path getters in ts-toolbelt. Deep partial updates are possible too with recursive types (check ramda types).

@anilanar Yes, I'm aware about recursive types and been using TS-toolbelt for creating better types for firestore. Sorry I got confused with your first example, but the second one you provided actually shows how to use Path. In fact I have already been using it.

I'm aware that sometimes a better API design would be ideal, but unfortunately that ship has long sailed and firestore cannot (won't) introduce breaking changes. See for example: https://github.com/googleapis/nodejs-firestore/issues/1000 or https://github.com/googleapis/nodejs-firestore/issues/1058

You are right that it would've been much better if update accepted a DeepPartial object, instead of this flattened deep partial version. The same goes to some other of their Apis.

My proposal was to introduce another library that wraps the original, with minor runtime overhead; which would consist of mostly glue code.

My observation is that libraries that were designed around dynamism of Javascript have more type safety problems than that can be solved with additional TS features and with only .d.ts definitions; unless they modify their APIs in a non-backward compatible way just to cater to TS users. JS library authors that are not willing to maintain TS types are already complaining about their user-bases requesting such changes on Twitter frequently; and a strong voice against TS is forming up in social media.

Issues that you linked seem to be of the same nature.

TS promised best-effort ā€œfree interoperabilityā€ with JS, but achieving top-notch type safety is not promised or guaranteed to be free.

(You are proficient in TS and most things Iā€™m saying is obvious to you, but Iā€™m writing for other participants in this thread waiting for this feature to come to life. Iā€™m hopefully helping others and maintainers understand what would make JS/TS better overall than pushing new features with hasty syntax decisions).

@anilanar I get your point, however the firestore library has been written in TS since the first commit. I get that the library may have been developed earlier, but at least that commit date coincides with the launch of firestore.

See https://github.com/googleapis/nodejs-firestore/blob/v0.8.0/src/reference.js#L452-L461

Since then the method has changed and the library has seen 3 bumps of major versions. So, I think it's not just a matter of JS vs TS, but also about having the right tools to express some ideas. Also, TS has evolved quite rapidly in the past years (2.5 to 3.8 for the library in question), so it's also not as easy to adapt large codebases, specially those with a larger userbase.

That said, and given that JS and TS library maintainers are struggling in some cases to cater to certain public, maybe it'd be great to have a list of patterns and antipatterns for typesafety.

Recently needed something like this myself, as I described in a SO posting:
https://stackoverflow.com/questions/61843772/typescript-infer-key-names-for-returned-object-from-function-input-argument

For my react hook I'd like to be able to dictate the name of the returned objects keys for easier destructuring.

const [users, { append }] = useList(initialUsers)
const [admins, { append }] = useList(initialAdmins)

This wouldn't work of course, since append would be defined twice.

One solution would be to rename (one or both of) the returned object keys during the destructing.

However I would like to implement it like this:

const [users, { appendUsers }] = useList(initialUsers, "Users")
const [admins, { appendAdmins }] = useList(initialAdmins, "Admins")

For that I created this TS function signature:

function useList<L extends any>({ initial = [], name = "" }: Options<L> = {}): [ L[], any ] {
   // ...
  return [
    items,
    {
      ["append" + name]: (newItem:L) => set([...items, newItem]),
    },
  ]
}

This works, but the returned object keys are not type checked.

I'd like to define the signature of each returned key in the object, and the signature of its value.
At the very least, the number of returned keys should be defined.

Without the dynamic part, that comes via function argument the return signature could be changed to:

[ L[], {
  append: ((T) => void)
} ]

But I'd need something like:

[ L[], {
  ["append" +  name]: ((T) => void)
} ]

I think for TS compiler to accept a key generator function would be best and most universal? Something like

const key = (context, prefix): string => `${prefix}${context.args['name'].name}`
[ L[], {
  [key("append")]: ((T) => void)
} ]

The key generator function would have to return a string, and take a TS context for the currently parsed function as its first argument. All the remaining arguments are the ones passed in when calling the function.

Any pointers by people deep into the TS parser and I'd give a PR a try.

For my react hook I'd like to be able to dictate the name of the returned objects keys for easier destructuring.

const [users, { append }] = useList(initialUsers)
const [admins, { append }] = useList(initialAdmins)

This wouldn't work of course, since append would be defined twice.

Can't you just

const [users, { append: appendUsers }] = useList(initialUsers)
const [admins, { append: appendAdmins }] = useList(initialAdmins)

?

@pke I was thinking the same thing about a key generator function, but that would mean that typescript is executing arbitrary code in the types definitions themselves. It would have to be able to run the code sandboxed and side-effect free, otherwise it is a potential attack vector.

@andrevmatos of course he can. But this wasn't the point of following example:

const [users, { append**Users** }] = useList(initialUsers, **"Users"**)
const [admins, { append**Admins** }] = useList(initialAdmins, **"Admins"**)

Can't you just

const [users, { append: appendUsers }] = useList(initialUsers)
const [admins, { append: appendAdmins }] = useList(initialAdmins)

Of course, that's what I mentioned in my OP. And that is the way I am doing it for now to stay type safe.

@pke I wouldn't do a PR unless there is an issue asking for contribution - they simply won't merge it. The team has been discussing the possibility of doing key augments (see link further up), but it probably will open up the door do slowing down the compiler. So its been kicked into the long-grass again.

I've run into a use case where I need this. I have a situation where I need to transform keys of an object with just a simple concatenation. Turn ${keyof T} into ${keyof T}_changed.

Generally it seems that any code that takes a definition object and generates a regular interface to work with that object based on its keys is impossible to type right now. It's a shame we can't do this yet! All sorts of model building / database applications are likely un-typeable because of this.

You can cast?:

type StyleType = key of styles

const styles = {
  base: {},
  primary: {},
  seconary: {},
  primaryHover: {},
}

const MyButton = ({type} : {type: string}]) => (
  // ...
  return <Button style={
    ...styles.base,
    ...styles[props.type],
    hover && ...styles[type + "Hover" as StyleType],
  }/>
)

every time you use as you say to typescript: I can't prove you that type is correct, but "Trust Me, I'm an Engineer Ā©".

I'd like to have an ability to express my intentions with a language syntax and don't use as in any case.

I know, it was a lame attempt to solve the problem.

I've run into a use case where I need this. I have a situation where I need to transform keys of an object with just a simple concatenation. Turn ${keyof T} into ${keyof T}_changed.

I need this for almost the exact same reason. Adding "meta" fields corresponding to existing fields in an object. Our current code works by using unsafe casts.

I would love to be able to do something like this:

interface WithMeta<T extends {}> {
    [K in keyof T]: T[K];
    [K+'_meta' for K in keyof T]: MetaInfo;
}

@sk- Although I donā€™t prefer object oriented APIs over pure functional ones, TS is currently closer to OOP languages feature-wise. So for best practices, I would look at other OOP sound languages, without taking any JS/dynamism specific shortcuts.

JS libs had always been conformist to the idea that a library must have an easy to understand README.md; that one function should be able to handle everything with tens of overloads if not hundreds (e.g. moment.js).

Encoding valuable information in strings, I accept, was another pattern JS libs have been practicing; but I think those practices must be abolished simply because even if TS supports typelevel string manipulations, interop with other communities and languages will be terrible.

Syntax fetishism to the degree of encoding information in strings was never a good idea (typewise) and will never be a good idea until itā€™s proven to be a practical approach by multiple languages. Can TS take that risk by itself?

Did someone do the theoretical background work on typelevel string operations and how they would work with polymorphism? Would that require backtracking in the type resolver? Does TS type resolver support backtracking?

As strings are (in theory) list of chars, can we somehow reuse type machinery that we already have for arrays/tuples with strings?

I know that everybody watching this thread are trying to solve their real world problems for their real world projects, but those projects will be around for couple for years maybe. The language will potentially stay around longer, keep that in mind.

@anilanar

Would that require backtracking in the type resolver? Does TS type resolver support backtracking?

I think it does - TS has just changed their internal mapping (in 3.9) to:

/* @internal */
const enum TypeMapKind {
  Simple,
  Array,
  Function,
  Composite,
  Merged,
}

/* @internal */
type TypeMapper =
  | { kind: TypeMapKind.Simple, source: ts.Type, target: ts.Type }
  | { kind: TypeMapKind.Array, sources: readonly ts.Type[], targets: readonly ts.Type[] | undefined }
  | { kind: TypeMapKind.Function, func: (t: ts.Type) => ts.Type }
  | { kind: TypeMapKind.Composite | TypeMapKind.Merged, mapper1: TypeMapper, mapper2: TypeMapper };

It used to be a simple function to map A->B, but it looks like it now structures the mapping in a far more verbose and structured way. Its possible this work will make doing mutations easier, as the mappings are recursive. At first glance these should be "invertable" if the reverse B->A is needed.

If the TS team exposed an ability to manipulate the bindings/mappings and/or "rebind" during a transformation (plugins) its possible the this issue (Augmenting the Keys) could be done in Userland.

There seem to be an awful lot of issues being marked as a duplicate of this issue. Many of them propose a feature I would call "Concatenated Types" which is, in my opinion, not at all covered by this proposal.

The title of this issue suggests that this feature should only be enabled during type mapping, which would make many _"Concatenated Types"_ impossible. For example, without going to the much more complicated regex types, simple types like this would be incredibly helpful:

// Helper Types
type Hex = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "A" | "B" | "C" | "D" | "E" | "F";
type Numeric = "0" | "1" | "2" | "3" | "4" | "5" |  "6" | "7" | "8" | "9";

// Concatenated Types
type Color = "#" + Hex + Hex + Hex + Hex + Hex + Hex | "#" + Hex + Hex + Hex;
type Email = string + "@" + string + "." + string;
type ZipCode = Numeric + Numeric + Numeric + Numeric + Numeric;

If you'd want to go really crazy, a repeat operator could also be helpful, shortening the Color example to

type Color = "#" + Repeat<Hex, 6> | "#" + Repeat<Hex, 3>

or something similar (I realize that breaks the usual Standard for Utility Types, as "6" is not strictly a type, so just sticking to manually repeating is fine). Another repeat operator could be

type ZipCode = Numeric * 5

which could offer an _"infinite repeat"_ option for something like

type FloatString = Numeric + "." + Numeric * *

but it would be perfectly reasonable to reserve the * operator for some other bright idea in the future. Of course the * could also be used in the Repeat<> thingymajig making the second argument even less type-like.

This would also change the way this feature is used in mapped types. The original example would then more likely become something like this:

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

With the addition of optional parts, _Concatented Types_ could become even more useful for purposes like

// Simple concatenated type
type RgbValue = Numeric * 2 | "1" + Numeric * 2 | "2" + ("0" | "1" | "2" | "3" | "4") + Numeric | "25" + ("0" | "1" | "2" | "3" | "4" | "5");
// Concatenated types with optional type
type AlphaValue = "0" | "1" | "0"? + "." + Numeric * *;
type Color = "#" + Hex * 6 | "#" + Hex * 3 | "rgba(" + (RgbValue + ", ") * 2 + RgbValue + (", " + AlphaValue)? + ")";

which is not the most beautiful thing, but it would make stuff like this at least possible without writing 16.5 Million Cases.

All this would of course only sense on string types, just like it is the case with the original proposal. Something like type Weight = number + "kg" would be nice, but I don't see how the type safety would work as any number + string is simply a string, and has strictly speaking nothing to do with a number anymore.

Personally I think what you're describing is blurring the line between structural type and value/formatting. Ultimately TS is to define structure. While TS does blur the line a little with strings, it only does so in-so-far as the strings boil down to a constant.

The "structure" in your examples could perfectly be defined (and works now) via Tuples.

type Hex = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "A" | "B" | "C" | "D" | "E" | "F";
type Numeric = "0" | "1" | "2" | "3" | "4" | "5" |  "6" | "7" | "8" | "9";

type RGB = ["#", Hex, Hex, Hex, Hex, Hex, Hex];
type RGBA = ["#", Hex, Hex, Hex, Hex, Hex, Hex, Hex, Hex];

type Color = RGB | RGBA;
type Email = [string, "@", string, ".", string];
type ZipCode = [Numeric, Numeric, Numeric, Numeric, Numeric];

there is also some support for repeating, with tuple rest operator:

type FloatString  = [Numeric, '.', ...Numeric[]];

Although it does not currently let you do the following (others trying to solve it):

type X3<T> = [T, T, T];
type X6<T> = [T, T, T, T, T, T];

type Color2 = ["#", ...X6<Hex>] | ["#", ...X3<Hex>] // error

In my opinion you would be better served by asking for typescript to support strings as an ArrayLike such that:

let a: FloatString = "1.23"; 

is valid - that way, even though you're working with strings - they can be matched against tuple types. i.e. for any string "1.23" it is implicitly treated as a Tuple ["1", ".", "2", "3"] during evaluation against another Tuple type.

Supporting strings as an ArrayLike seems like a great idea. Tuples alone for this case is more of a hack than anything else. ['#' ,'0', '1', '2', '3', '4', '5'] is less readable than simply defining an RGB tuple or class and then transforming it into a CSS string.

It's perfectly reasonable to not support concatenating string types due to the problems you described. Nevertheless I think it would be a great feature and thought I should mention it here, since most of the issues related to it are marked as duplicate of this issue. Regex types are the alternative, and in my opinion they go too far and open the door to performance degradation.

Note: I stumbled upon this problem and I want a proper solution too.

In the meantime, for people that are having problems with strings, I found an alternative way of thinking about this with type guards for my problem:

type pet = "dog" | "cat" | "default";
const intensity: Record<pet, number> = { dog: 5, cat: 9, default: 12 };

const isPet = (s: string): s is pet => intensity[s as pet] !== undefined;

function getIntensityFromLabel(label: string): number {
  if (isPet(label)) return intensity[label];

  const [reversedDirectionLabel] = label.split("Reversed");
  if (isPet(reversedDirectionLabel))
    return intensity[reversedDirectionLabel] * -1;
  return intensity.default;
}

getIntensityFromLabel("dogReversed"); // -5
getIntensityFromLabel("dog"); // 5
getIntensityFromLabel("iLoveCake"); // 12

The code should be self-explanatory, but the point is that in my function, which would need an augmented type, I use a more loose type (string), and then use type guards to get what I want.

Keep in mind that this approach absolutely does not solve the issue, but for some use cases, type guards are good friends.

I think the minimum required feature here which seems to me pretty reasonable is literal string type concatenation. I believe any other literal type manipulation is far too complex.
So generally just:

type a = 'hhh';
type b = 'ggg';
type ab = a + b; // hhhggg

Of course with generic support šŸ˜„

type Email = [string, "@", string, ".", string];

I'd be fine with that, it it would work:

email = "[email protected]"
Type '"[email protected]"' is not assignable to type '[string, "@", string, ".", string]'.ts(2322)

When will this feature be added?, it's too necessary for me.

I'd also love to see something like this. Right now TypeScript is unable to cope with any code that generates functions at runtime with "template-like" dynamic function names. E.g. code that takes a label/key such as "Users" and creates functions for fetchUsers(), selectUsers(), selectIsUsersLoading(), etc.

Currently, the only possible way to handle it is to painstakingly define interfaces for every dynamic method, for every varying label/key. Which is obviously infeasible.

The workaround for that case is to have the API be like api.users.fetch(), instead of api.fetchUsers(). That's reasonably supported by existing TS machinery, and I find is cleaner in implementation, anyway. (I know not all APIs are able/willing to make those sort of breaking changes)

@Retsam right, obviously that would be the preferred option. But in cases where you can't make that breaking change or don't have control of a library that works this way, changing the API isn't an option.

This suggestion is now implemented in #40336.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

fwanicka picture fwanicka  Ā·  3Comments

jbondc picture jbondc  Ā·  3Comments

bgrieder picture bgrieder  Ā·  3Comments

manekinekko picture manekinekko  Ā·  3Comments

wmaurer picture wmaurer  Ā·  3Comments