typescript template literal in interface key error - javascript

Typescript v4.4.3
Reproducible Playground Example
--
interface IDocument {
[added_: `added_${string}`]: number[] | undefined;
}
const id = 'id';
const document: IDocument = {
[`added_${id}`]: [1970]
}
What i've tried:
I've confirmed that id in my code is a string.
This happens when running tsc not just in VSCode warnings
[`added_abc`]: [1] // no error
[`added_${'abc'}`]: [1] // errors
[`added_${stringVariable}`] // errors
Is there some restrictions of using template literals or anything else I can investigate to diagnose this?
'string' and '`added_${string}`' index signatures are incompatible.
Type 'string | string[] | number[]' is not assignable to type 'number[] | undefined'.
Type 'string' is not assignable to type 'number[] | undefined'.ts(2322)

The issue is that computed keys of types that are not single literal types are widened to string, and such object literals that use them will end up being given a full string index signature instead of anything narrower. So something like {[k]: 123} will be given a narrow key if k is of type "foo" ({foo: number}), but if k is of a union type type "foo" | "bar" or a pattern template literal type (as implemented in ms/TS#40598) like `foo${string}`, then it will get a full string index signature ({[x: string]: number}).
There is an open issue at microsoft/TypeScript#13948 asking for something better here; it's been around a long time and originally was asking only about unions of literals. Now that pattern template literals exist this behavior is even more noticeable. For now there is no built-in support in the language to deal with this.
In your code, tech1.uuid is of type string... not a string literal type, because the compiler infers string property types as string and not more narrowly. If you want a narrower literal type there, you might want to give tech's initializer a const assertion:
const tech1 = {
uuid: '70b26275-5096-4e4b-9d50-3c965c9e5073',
} as const;
/* const tech1: {
readonly uuid: "70b26275-5096-4e4b-9d50-3c965c9e5073";
} */
Then to get the computed key to be a single literal, you will need another const assertion to tell the compiler that is should actually process the template literal value `added_${tech1.uuid}` as a template literal type:
const doc: IDocument = {
name: "",
[`added_${tech1.uuid}` as const]: [19700101], // <-- const assert in there
}; // okay
(They almost made such things happen automatically without a const assertion, but it broke too much code and was reverted in microsoft/TypeScript#42588).
If you need tech1.uuid to remain string and want more strongly-typed computed keys, then you will need to work around it with a helper function. Here's one which takes a key of type K and a value pf type V and returns an object whose type is a type whose keys are in K and whose values are in V. (It distributes over unions, since kv(Math.random()<0.5 ? "a" : "b", 123) should have type {a: number} | {b: number} and not {a: number, b: number}:
function kv<K extends PropertyKey, V>(k: K, v: V):
{ [P in K]: { [Q in P]: V } }[K] {
return { [k]: v } as any;
}
You can see that it behaves as desired with a pattern template literal key:
const test = kv(`added_${tech1.uuid}` as const, [19700101]);
/* const test: { [x: `added_${string}`]: number[]; } */
And so you can use it along with Object.assign() to build the object you want as an IDocument:
const doc: IDocument = Object.assign(
{ name: "" },
kv(`added_${tech1.uuid}` as const, [19700101])
)
(Note that while you should be able to write {name: "", ...kv(`added_${tech1.uuid}` as const, [19700101])}, this isn't really working safely because the index signature is removed. See microsoft/TypeScript#42021 for more information.)
This may or may not be worth it to you; probably you can just write a type assertion and move on:
const doc = {
name: "",
[`added_${tech1.uuid}`]: [19700101],
} as IDocument;
This is less safe than the prior solutions but it's very easy.
Playground link to code

You need to assure TypeScript that tech1.uuid is a constant value.
interface IDocument {
name: string;
[added_: `added_${string}`]: number[] | undefined;
}
const tech1 = {
uuid: '70b26275-5096-4e4b-9d50-3c965c9e5073',
} as const;
const doc: IDocument = {
name: "",
[`added_${ tech1.uuid }` as const]: [19700101],
};
Playground

Related

Get Javascript interface keys as string (like an Enum)

My request is actually really simple but I have found almost nothing of the sort online. Picture this, you have an interface like so:
interface MyInterface: {
propertyA: number;
propertyB: string;
propertyC: boolean;
}
Now you want to use the name of a field to access its value dynamically so you do something like object['propertyA'], where object is of type MyInterface.
However, passing 'propertyA' as a raw string is impractical and dangerous because if you rename it or change the interface the code won't work. (And it doesn't get picked up by typescript but I'm not sure this is possible anyway).
Ideally, I would like to do something like this MyInterface.propertyA, I was saddened that this doesn't seem to exist, I don't see any reason why it shouldn't.
And now before you say that's what object.propertyA is for, I have simplified the concept to make it easy to understand but the use case is not exactly this. I might pass fields of an interface as strings from react components within the JSX to make use of a reusable method or I could be checking whether a string matches a subset of keys in an interface to display errors accordingly.
Autogenerating an enum from the interface/class is not a bad solution although not ideal as it still incurs an overhead but as long as it doesn't need to be updated each time the interface changes and it doesn't depend on the type of the interface's properties then that's not bad.
Interfaces don't exist at runtime, so there's no way to get the property names of an interface as runtime values. What you can do, though, is go the other way: Have a "model" object:
const myInterfaceModel = {
propertyA: 42,
propertyB: "x",
propertyC: true,
};
Then define the interface using that object:
type MyInterface = typeof myInterfaceModel;
That defines MyInterface just as it is in your question.
Then you can create an object that has the property names as strings, like this:
const myInterfaceKeys = Object.fromEntries(
Object.keys(myInterfaceModel).map((key) => [key, key])
);
Now, if we just did that, myInterfaceKeys would be (effectively) Record<string, string>, which isn't useful for devex. But we can do better by using a mapped type on it:
type KeyNames<T> = {
[Key in keyof T]: Key;
};
// ...
const myInterfaceKeys = Object.fromEntries(
Object.keys(myInterfaceModel).map((key) => [key, key])
) as KeyNames<MyInterface>;
Now, its type matches its runtime value, and we get useful autocompletion:
Example usage (just a basic function, but could as easily be a React component):
function example(obj: MyInterface, key: keyof MyInterface) {
console.log(obj[key]);
}
declare let exampleObject: MyInterface;
example(exampleObject, myInterfaceKeys.propertyA);
Here's that all together (playground link):
type KeyNames<T> = {
[Key in keyof T]: Key;
};
const myInterfaceModel = {
propertyA: 42,
propertyB: "x",
propertyC: true,
};
type MyInterface = typeof myInterfaceModel;
// ^? type MyInterface = { propertyA: number, propertyB: string; propertyC : boolean }
const myInterfaceKeys = Object.fromEntries(
Object.keys(myInterfaceModel).map((key) => [key, key])
) as KeyNames<MyInterface>;
myInterfaceKeys
// ^−−−− Type a period after this to see the autocompletion
Usually I'd just use string literals (example(exampleObject, "propertyA")) since the IDE will offer the appropriate choices, and since keyof MyInterface is restrictive, if you remove a property, any code that referenced it with a string literal would stop compiling. But if you want that object with the keys on it, you can do the above to achieve that. (Also, I just tried, and if you use your object of keys, renaming a property works throughout, whereas the string literals didn't.)
Are you looking for something like this?
enum Direction {
Up = "u",
Down = "d",
Left = "l",
Right = "r",
}
interface MyDirection {
[Direction.Up]:string;
[Direction.Down]:number;
}
const obj:MyDirection = {
[Direction.Up]: 'up',
[Direction.Down]:12,
}
console.log(obj[Direction.Down], Direction.Down)

Is there a way to show a TypeScript error when an unknown property is passed in an object to function? [duplicate]

Here is my code
async getAll(): Promise<GetAllUserData[]> {
return await dbQuery(); // dbQuery returns User[]
}
class User {
id: number;
name: string;
}
class GetAllUserData{
id: number;
}
getAll function returns User[], and each element of array has the name property, even if its return type is GetAllUserData[].
I want to know if it is possible "out of the box" in TypeScript to restrict an object only to properties specified by its type.
I figured out a way, using built-in types available since TypeScript version 3, to ensure that an object passed to a function does not contain any properties beyond those in a specified (object) type.
// First, define a type that, when passed a union of keys, creates an object which
// cannot have those properties. I couldn't find a way to use this type directly,
// but it can be used with the below type.
type Impossible<K extends keyof any> = {
[P in K]: never;
};
// The secret sauce! Provide it the type that contains only the properties you want,
// and then a type that extends that type, based on what the caller provided
// using generics.
type NoExtraProperties<T, U extends T = T> = U & Impossible<Exclude<keyof U, keyof T>>;
// Now let's try it out!
// A simple type to work with
interface Animal {
name: string;
noise: string;
}
// This works, but I agree the type is pretty gross. But it might make it easier
// to see how this works.
//
// Whatever is passed to the function has to at least satisfy the Animal contract
// (the <T extends Animal> part), but then we intersect whatever type that is
// with an Impossible type which has only the keys on it that don't exist on Animal.
// The result is that the keys that don't exist on Animal have a type of `never`,
// so if they exist, they get flagged as an error!
function thisWorks<T extends Animal>(animal: T & Impossible<Exclude<keyof T, keyof Animal>>): void {
console.log(`The noise that ${animal.name.toLowerCase()}s make is ${animal.noise}.`);
}
// This is the best I could reduce it to, using the NoExtraProperties<> type above.
// Functions which use this technique will need to all follow this formula.
function thisIsAsGoodAsICanGetIt<T extends Animal>(animal: NoExtraProperties<Animal, T>): void {
console.log(`The noise that ${animal.name.toLowerCase()}s make is ${animal.noise}.`);
}
// It works for variables defined as the type
const okay: NoExtraProperties<Animal> = {
name: 'Dog',
noise: 'bark',
};
const wrong1: NoExtraProperties<Animal> = {
name: 'Cat',
noise: 'meow'
betterThanDogs: false, // look, an error!
};
// What happens if we try to bypass the "Excess Properties Check" done on object literals
// by assigning it to a variable with no explicit type?
const wrong2 = {
name: 'Rat',
noise: 'squeak',
idealScenarios: ['labs', 'storehouses'],
invalid: true,
};
thisWorks(okay);
thisWorks(wrong1); // doesn't flag it as an error here, but does flag it above
thisWorks(wrong2); // yay, an error!
thisIsAsGoodAsICanGetIt(okay);
thisIsAsGoodAsICanGetIt(wrong1); // no error, but error above, so okay
thisIsAsGoodAsICanGetIt(wrong2); // yay, an error!
Typescript can't restrict extra properties
Unfortunately this isn't currently possible in Typescript, and somewhat contradicts the shape nature of TS type checking.
Answers in this thread that relay on the generic NoExtraProperties are very elegant, but unfortunately they are unreliable, and can result in difficult to detect bugs.
I'll demonstrate with GregL's answer.
// From GregL's answer
type Impossible<K extends keyof any> = {
[P in K]: never;
};
type NoExtraProperties<T, U extends T = T> = U & Impossible<Exclude<keyof U, keyof T>>;
interface Animal {
name: string;
noise: string;
}
function thisWorks<T extends Animal>(animal: T & Impossible<Exclude<keyof T, keyof Animal>>): void {
console.log(`The noise that ${animal.name.toLowerCase()}s make is ${animal.noise}.`);
}
function thisIsAsGoodAsICanGetIt<T extends Animal>(animal: NoExtraProperties<Animal, T>): void {
console.log(`The noise that ${animal.name.toLowerCase()}s make is ${animal.noise}.`);
}
const wrong2 = {
name: 'Rat',
noise: 'squeak',
idealScenarios: ['labs', 'storehouses'],
invalid: true,
};
thisWorks(wrong2); // yay, an error!
thisIsAsGoodAsICanGetIt(wrong2); // yay, an error!
This works if at the time of passing an object to thisWorks/thisIsAsGoodAsICanGet TS recognizes that the object has extra properties. But in TS if it's not an object literal, a value can always have extra properties:
const fun = (animal:Animal) =>{
thisWorks(animal) // No Error
thisIsAsGoodAsICanGetIt(animal) // No Error
}
fun(wrong2) // No Error
So, inside thisWorks/thisIsAsGoodAsICanGetIt you can't trust that the animal param doesn't have extra properties.
Solution
Simply use pick (Lodash, Ramda, Underscore).
interface Narrow {
a: "alpha"
}
interface Wide extends Narrow{
b: "beta"
}
const fun = (obj: Narrow) => {
const narrowKeys = ["a"]
const narrow = pick(obj, narrowKeys)
// Even if obj has extra properties, we know for sure that narrow doesn't
...
}
Typescript uses structural typing instead of nominal typing to determine type equality. This means that a type definition is really just the "shape" of a object of that type. It also means that any types which shares a subset of another type's "shape" is implicitly a subclass of that type.
In your example, because a User has all of the properties of GetAllUserData, User is implicitly a subtype of GetAllUserData.
To solve this problem, you can add a dummy property specifically to make your two classes different from one another. This type of property is called a discriminator. (Search for discriminated union here).
Your code might look like this. The name of the discriminator property is not important. Doing this will produce a type check error like you want.
async function getAll(): Promise<GetAllUserData[]> {
return await dbQuery(); // dbQuery returns User[]
}
class User {
discriminator: 'User';
id: number;
name: string;
}
class GetAllUserData {
discriminator: 'GetAllUserData';
id: number;
}
I don't think it's possible with the code structure you have. Typescript does have excess property checks, which sounds like what you're after, but they only work for object literals. From those docs:
Object literals get special treatment and undergo excess property checking when assigning them to other variables, or passing them as arguments.
But returned variables will not undergo that check. So while
function returnUserData(): GetAllUserData {
return {id: 1, name: "John Doe"};
}
Will produce an error "Object literal may only specify known properties", the code:
function returnUserData(): GetAllUserData {
const user = {id: 1, name: "John Doe"};
return user;
}
Will not produce any errors, since it returns a variable and not the object literal itself.
So for your case, since getAll isn't returning a literal, typescript won't do the excess property check.
Final Note: There is an issue for "Exact Types" which if ever implemented would allow for the kind of check you want here.
Following up on GregL's answer, I'd like to add support for arrays and make sure that if you've got one, all the objects in the array have no extra props:
type Impossible<K extends keyof any> = {
[P in K]: never;
};
export type NoExtraProperties<T, U extends T = T> = U extends Array<infer V>
? NoExtraProperties<V>[]
: U & Impossible<Exclude<keyof U, keyof T>>;
Note: The type recursion is only possible if you've got TS 3.7 (included) or above.
The accepted answer, with a discriminator, is right. TypeScript uses structural typing instead of nominal typing. It means that the transpiler will check to see if the structure match. Since both classes (could be interface or type) has id of type number it matches, hence interchangeable (this is true one side since User is having more properties.
While this might be good enough, the issue is that at runtime the returned data from your method getAll will contains the name property. Returning more might not be an issue, but could be if you are sending back the information somewhere else.
If you want to restrict the data to only what is defined in the class (interface or type), you have to build or spread a new object manually. Here is how it can look for your example:
function dbQuery(): User[] {
return [];
}
function getAll(): GetAllUserData[] {
const users: User[] = dbQuery();
const usersIDs: GetAllUserData[] = users.map(({id}) => ({id}));
return usersIDs;
}
class User {
id: number;
name: string;
}
class GetAllUserData {
id: number;
}
Without going with the runtime approach of pruning the fields, you could indicate to TypeScript that both classes are different with a private field. The code below won't let you return a User when the return type is set to GetAllUserData
class User {
id: number;
name: string;
}
class GetAllUserData {
private _unique: void;
id: number;
}
function getAll(): GetAllUserData[] {
return dbQuery(); // Doesn't compile here!
}
I found this another workaround:
function exactMatch<A extends C, B extends A, C = B>() { }
const a = { a: "", b: "", c: "" }
const b = { a: "", b: "", c: "", e: "" }
exactMatch<typeof a, typeof b>() //invalid
const c = { e: "", }
exactMatch<typeof a, typeof c>() //invalid
const d = { a: "", b: "", c: "" }
exactMatch<typeof a, typeof d>() //valid
const e = {...a,...c}
exactMatch<typeof b, typeof e>() //valid
const f = {...a,...d}
exactMatch<typeof b, typeof f>() //invalid
See the original Post
Link to Playground
As an option, you can go with a hack:
const dbQuery = () => [ { name: '', id: 1}];
async function getAll(): Promise<GetAllUserData[]> {
return await dbQuery(); // dbQuery returns User[]
}
type Exact<T> = {[k: string | number | symbol]: never} & T
type User = {
id: number;
name: string;
}
type GetAllUserData = Exact<{
id: number;
}>
Error this produces:
Type '{ name: string; id: number; }[]' is not assignable to type '({ [k: string]: never; [k: number]: never; [k: symbol]: never; } & { id: number; })[]'.
Type '{ name: string; id: number; }' is not assignable to type '{ [k: string]: never; [k: number]: never; [k: symbol]: never; } & { id: number; }'.
Type '{ name: string; id: number; }' is not assignable to type '{ [k: string]: never; [k: number]: never; [k: symbol]: never; }'.
Property 'name' is incompatible with index signature.
Type 'string' is not assignable to type 'never'.
When using types instead of interfaces, the property are restricted. At least in the IDE (no runtime check).
Example
type Point = {
x: number;
y: number;
}
const somePoint: Point = {
x: 10,
y: 22,
z: 32
}
It throws :
Type '{ x: number; y: number; z: number; }' is not assignable to type 'Point'. Object literal may only specify known properties, and 'z' does not exist in type 'Point'.
I think types are good for defining closed data structures, compared to interfaces. Having the IDE yelling (actually the compiler) when the data does not match exactly the shape is already a great type guardian when developping

TypeScript - How do you chain accessing optional nested type properties?

I have a Client class that stores caches of other objects that the application needs to keep in memory. The structure of the object's cache is developer-defined. For example, if we have a cache of Example objects:
class Example {
property1: string;
property2: string;
}
The developer might only want property1 cached.
import { EventEmitter } from "events";
// Constructor options for `Client`
interface ClientData {
cacheStrategies?: CacheStrategies;
}
// How various objects should be cached
interface CacheStrategies {
example?: ExampleCacheStrategies;
...
}
// Metadata for how each type of object should be cached
interface ExampleCacheStrategies {
cacheFor?: number;
customCache?: ExampleCustomCacheData;
}
// The custom structure of what parts of `example` objects should be cached
interface ExampleCustomCacheData {
property1?: boolean;
property2?: boolean;
}
// The object stored in `Client.exampleCache`, based on the custom structure defined in `ExampleCustomCacheData`
interface CustomExampleData<CachedExampleProperties extends ExampleCustomCacheData> {
property1: CachedExampleProperties["property1"] extends true ? string /* 1 */ : undefined;
property2: CachedExampleProperties["property2"] extends true ? string : undefined;
}
class Client<ClientOptions extends ClientData> extends EventEmitter {
// The map's value should be based on the custom structure defined in `ExampleCustomCacheData`
exampleCache: Map<string, CustomExampleData<ClientOptions["cacheStrategies"]["example"]["customCache"]>>;
constructor(clientData: ClientOptions) {
super();
this.exampleCache = new Map();
}
}
const client = new Client({
cacheStrategies: {
example: {
/**
* The properties of `example` objects that should be cached
* This says that `property1` should be cached (string (1))
*/
customCache: {
property1: true, // (2)
... // (3)
}
}
}
});
client.exampleCache.set("123", {
property1: "value"
});
const exampleObject = client.exampleCache.get("123");
if (exampleObject) {
// Should be `string` instead of `string | undefined` (2)
console.log(exampleObject.property1);
// `string | undefined`, as expected since it's falsey (3)
console.log(exampleObject.property2);
}
As explained in the comments above the console.log()s, the goal is for objects that are pulled from the cache to have property1 be a string instead of string | undefined.
The problem is that exampleCache: Map<string, CustomExampleData<ClientOptions["cacheStrategies"]["example"]["customCache"]>>; doesn't work since both ClientOptions["cacheStrategies"] and ClientOptions["cacheStrategies"]["example"] are optional. The following doesn't work either:
exampleCache: Map<string, CustomExampleData<ClientOptions["cacheStrategies"]?.["example"]?.["customCache"]>>;
It errors with '>' expected at ?.. How can I solve this?
Syntax like the optional chaining operator ?. or the non-null assertion operator ! only applies to value expressions that will make it through to JavaScript in some form. But you need something that works with type expressions which exist only in the static type system and are erased when transpiled.
There is a NonNullable<T> utility type which is the type system analog of the non-null assertion operator. Given a union type T, the type NonNullable<T> will be the same as T but without any union members of type null or undefined:
type Foo = string | number | undefined;
type NonNullableFoo = NonNullable<Foo>;
// type NonNullableFoo = string | number
In fact, the compiler actually uses it to represent the type of an expression that has the non-null assertion operator applied to it:
function nonNullAssertion<T>(x: T) {
const nonNullX = x!;
// const nonNullX: NonNullable<T>
}
So, everywhere you have a type T that includes null or undefined and you would like to remove it, you can use NonNullable<T>. In your code, you will need to do it multiple times. In the interest of something like brevity (of code, not my explanation), let's use a shorter alias:
type NN<T> = NonNullable<T>;
and then
class Client<C extends ClientData> extends EventEmitter {
exampleCache: Map<string, CustomExampleData<
NN<NN<NN<C["cacheStrategies"]>["example"]>["customCache"]>>
>;
}
This compiles without error, and behaves how I think you'd like it:
console.log(exampleObject.property1.toUpperCase()); // string
console.log(exampleObject.property2); // undefined
Playground link to code

How would I implement this type in Typescript (from Flow?) [duplicate]

Is there a way to extend the built-in Record (or a { [key:string]: string } interface) where you also define some fixed keys and their types?
Let's say we have this:
const otherValues = {
some: 'some',
other: 'other',
values: 'values',
}
const composedDictionary = {
id: 1,
...otherValues
}
I want to define an interface for composedDictionary where id is typed as number (and number only) and everything else as string.
I've tried this:
interface ExtendedRecord extends Record<string, string> {
id: number;
}
and this:
interface MyDictionary {
[key: string]: string;
id: number;
}
Both fail with:
Property 'id' of type 'number' is not assignable to string index type 'string'
Any ideas?
Ideally the index signature should reflect any possible indexing operation result. If you access composedDictionary with an uncheckable string key the result might be number if that string is actually 'id' (eg: composedDictionary['id' as string], the typings would say this is string but at runtime it turns out to be number). This is why the type system is fighting you on this, this is an inconsistent type.
You can define your index to be consistent will all properties:
interface MyDictionary {
[key: string]: string | number;
id: number;
}
There is a loophole to the checks typescript does for index and property consistency. That loop hole is intersection types:
type MyDictionary = Record<string, string> & {
id: number
}
const composedDictionary: MyDictionary = Object.assign({
id: 1,
}, {
...otherValues
});
The compiler will still fight you on assignment, and the only way to create such an inconsistent object in the type system is by using Object.assign
As the other answer said, TypeScript does not support a type in which some properties are exceptions to the index signature... and thus there is no way to represent your MyDictionary as a consistent concrete type. The inconsistent-intersection solution ({[k: string]: string]} & {id: number}) happens to work on property reads, but is difficult to use with property writes.
There was an old suggestion to allow "rest" index signatures where you can say that an index signature is supposed to represent all properties except for those specified.
There's also a more recent (but possibly shelved) pair of enhancements implementing negated types and arbitrary key types for index signatures, which would allow you to represent such exception/default index signature properties as something like { id: number; [k: string & not "id"]: string }. But that doesn't compile yet (TS3.5) and may never compile, so this is just a dream for now.
So you can't represent MyDictionary as a concrete type. You can, however, represent it as a generic constraint. Using it suddenly requires that all your previously concrete functions must become generic functions, and your previously concrete values must become outputs of generic functions. So it might be too much machinery than it's worth. Still, let's see how to do it:
type MyDictionary<T extends object> = { id: number } & {
[K in keyof T]: K extends "id" ? number : string
};
In this case, MyDictionary<T> takes a candidate type T and transforms it into a version which matches your desired MyDictionary type. Then we use the following helper function to check if something matches:
const asMyDictionary = <T extends MyDictionary<T>>(dict: T) => dict;
Notice the self-referencing constraint T extends MyDictionary<T>. So here's your use case, and how it works:
const otherValues = {
some: "some",
other: "other",
values: "values"
};
const composedDictionary = asMyDictionary({
id: 1,
...otherValues
}); // okay
That compiles with no errors, because the parameter to asMyDictionary() is a valid MyDictionary<T>. Now let's see some failures:
const invalidDictionary = asMyDictionary({
id: 1,
oops: 2 // error! number is not a string
})
const invalidDictionary2 = asMyDictionary({
some: "some" // error! property id is missing
})
const invalidDictionary3 = asMyDictionary({
id: "oops", // error! string is not a number
some: "some"
})
The compiler catches each of those mistakes and tells you where the problem is.
So, this is the closest I can get to what you want in as of TS3.5. Okay, hope that helps; good luck!
Link to code

typescript dynamic and safe adding element to array

I'm porting a Javascript application to Typescript.
I have an object with many array properties containing data and a javscript function do access the data arrays. For simplicity I consider only the add method
function updateDataStructure(state, structureName, elem) {
state.data[structureName].push(elem);
return state.data[structureName];
}
Porting to typescript I'd like to introduce type safety and maintain generic code.
interface AA {
lep: string,
snap: number,
sul: boolean
}
interface BB {
p1: string;
val: number;
}
interface StateDataContainer {
aa: AA[],
bb: BB[],
}
class State {
data: StateDataContainer;
}
export type DataContainerProps = keyof StateDataContainer;
type ArrayElement<ArrayType> = ArrayType extends (infer ElementType)[] ? ElementType : never;
function updateDataStructure<K extends DataContainerProps, D extends ArrayElement<StateDataContainer[K]>>
(state: State, structureName: K, elem: D)
: StateDataContainer[K] {
// #ts-ignore
state.data[structureName].push(elem);
return state.data[structureName];
}
With this code I have a typed safe interface, the compiler checks the client code to add the proper objects into the right structure.
const state = new State();
let avv: BB;
let line: AA;
let v: BB[] = updateDataStructure(state, 'bb', avv!);
let l: AA[] = updateDataStructure(state, 'aa', line!);
with this solution I have to add use the // #ts-ignore annotation in the implementation code to avoid compiler errors like:
Argument of type 'D' is not assignable to parameter of type 'AA & BB'.
Type 'ArrayElement<StateDataContainer[K]>' is not assignable to type 'AA & BB'.
Type '{}' is not assignable to type 'AA & BB'.
Type '{}' is not assignable to type 'AA'.
Type 'AA | BB' is not assignable to type 'AA & BB'.
Is there a better approach to write this code?
I think the other issue is that TypeScript doesn't understand the semantics of your ArrayElement type, i.e. it doesn't know it means something that you can append to StateDataContainer[K].
One way to go around this issue is to define state in terms of D, i.e.
function updateDataStructure<K extends DataContainerProps, D extends ArrayElement<StateDataContainer[K]>>
(state: { data: { [key in K]: D[] } }, structureName: K, elem: D)
: D[] {
state.data[structureName].push(elem);
return state.data[structureName];
}
TypeScript playground: link
Your structureName param has the type 'aa' | 'bb', and your elem type is AA | BB. TypeScript currently doesn't narrow generics the way you would expect (see #23578, #24085).
In your example, note that the input/output types are still correct (you can't pass line in if your structureName is 'aa'). So, if you're willing to sacrifice some type safety in your function implementation, here's a hack to get you unblocked:
state.data[structureName as 'aa'].push(elem as AA);

Categories