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

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

Related

Generic Nested Value Getter in TypeScript

I'm trying to write a TS function which gets a nested value from given object. That object can be one of several types so I'm using generics. However, TS complains so I feel like I'm misunderstanding how generics work in TS:
interface BaseValueObject<T> {
value: T | null
}
type StringValue = BaseValueObject<string>
type NumberValue = BaseValueObject<number>
interface FormAData {
name: StringValue,
age: NumberValue
}
interface FormBData {
height: NumberValue
nickname: StringValue
}
interface FormA {
data: FormAData
}
interface FormB {
data: FormBData
}
type Form = FormA | FormB
const getFormValue =
<F extends Form, P extends keyof F['data']>(form: F, property: P) =>
form['data'][property]['value'] // Type 'P' cannot be used to index type 'FormAData | FormBData'
Desired usage:
const formARecord: FormA = {
data: {
name: {
value: 'Joe'
},
age: {
value: 50
}
}
}
const joesAge = getFormValue(formARecord, 'age')
console.log(joesAge) // 50
Playground
Solution
Here's what I ended up doing, similar to what #jered suggested in his answer:
playground
Basically the lesson is to make explicit any invariants you have your typings. In my case, I wasn't formally telling the compiler that every property FormAData and FormBData adhered to the same interface. I was able to do so by extending them from this base interface:
...
type Value = StringValue | NumberValue
interface BaseFormData {
[property: string]: Value
}
interface FormAData extends BaseFormData {
...
You should extend your declaration of generics to the "form" interfaces themselves.
In this case you need to give TypeScript a way to "infer" what the type of the data property of the form will be, in order for property to properly index it.
The way you have it written currently gives an error because you can't use keyof to extract the properties of a union type. Consider this example:
type Foo = {
fooProperty: string;
}
type Bar = {
barProperty: string;
}
type Baz = Foo | Bar;
type Qux = keyof Baz; // type Qux = never
What type is Qux supposed to be? It can't be the keys of two different types simultaneously, so it ends up being of type never which is not very useful for indexing properties of an object.
Instead, consider if your base Form type was itself a generic, that you know should always have a data property, but which the specific type of that data property is unknown until it is actually utilized. Fortunately, you could still constrain some aspects of data to ensure enforcements of its structure across your app:
interface FormDataType {
[key: string]: StringValue | NumberValue;
};
interface Form<T extends FormDataType> {
data: T
};
Then when you write your flavors of Form that have more specific type definitions for the data property, you can do so like this:
type FormA = Form<{
name: StringValue,
age: NumberValue
}>;
type FormB = Form<{
height: NumberValue
nickname: StringValue
}>;
In a way this is sort of like "extending" the type, but in a way that allows TypeScript to use the Form generic to infer (literally) the type of data later on.
Now we can rewrite the getFormValue() function's generic types to match the Form generics in the function signature. Ideally the return type of the function would be perfectly inferred just from the function parameters and function body, but in this case I didn't find a good way to structure the generics so that everything was seamlessly inferred. Instead, we can directly cast the return type of the function. This has the benefit of 1. still checking that form["data"] exists and matches the FormDataType structure we established earlier, and 2. inferring the actual type of the value returned from calling getFormValue(), increasing your overall type checking confidence.
const getFormValue = <
F extends Form<FormDataType>,
P extends keyof F["data"]
>(
form: F,
property: P
) => {
return form["data"][property].value as F["data"][P]["value"];
}
Playground
Edit: on further reflection the generics of the Form interface itself is not really necessary, you could do something else like declare a basic Form interface and then extend it with each specific form:
interface FormDataType {
[key: string]: StringValue | NumberValue;
}
interface Form {
data: FormDataType
};
interface FormA extends Form {
data: {
name: StringValue;
age: NumberValue;
}
};
interface FormB extends Form {
data: {
height: NumberValue;
nickname: StringValue;
}
};
const getFormValue = <
F extends Form,
P extends keyof F["data"]
>(
form: F,
property: P
) => {
return form["data"][property].value as F["data"][P]["value"];
}

typescript template literal in interface key error

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

Typescript: type of subclasses of an abstract generic class

I have a Base generic class:
abstract class BaseClass<T> {
abstract itemArray: Array<T>;
static getName(): string {
throw new Error(`BaseClass - 'getName' was not overridden!`);
}
internalLogic() {}
}
and inheritors:
type Item1 = {
name: string
}
class Child1 extends BaseClass<Item1> {
itemArray: Array<Item1> = [];
static getName(): string {
return "Child1";
}
}
type Item2 = {
name: number
}
class Child2 extends BaseClass<Item2> {
itemArray: Array<Item2> = [];
static getName(): string {
return "Child2";
}
}
Now I want to have to define an object with the inheritors as its values:
type IChildrenObj = {
[key: string]: InstanceType<typeof BaseClass>;
};
/*
The following error is received: Type 'typeof BaseClass' does not satisfy the constraint 'new (...args: any) => any'.
Cannot assign an abstract constructor type to a non-abstract constructor type. ts(2344)
*/
const Children: IChildrenObj = {
C1: Child1,
C2: Child2,
}
Lastly, I want to be able to use statics methods of the children, and also be able to create instances of them:
const child: typeof BaseClass = Children.C1;
/*
received the following error: Property 'prototype' is missing in type '{ getName: () => string; }' but required in type 'typeof BaseClass'. ts(2741)
*/
console.log(child.getName());
const childInstance: BaseClass = new child();
/*
The following 2 errors are received:
(1) Generic type 'BaseClass<T>' requires 1 type argument(s). ts(2314)
(2) Cannot create an instance of an abstract class. ts(2511)
Generic type 'BaseClass<T>' requires 1 type argument(s). ts(2314)
*/
Firstly, the type
type IChildrenObj = {
[key: string]: InstanceType<typeof BaseClass>; // instances?
};
is not appropriate to describe your Children object. Children stores class constructors while InstanceType<typeof BaseClass>, even if it worked for abstract classes (which, as you noted, it doesn't), would be talking about class instances. It would be closer to write
type IChildrenObj = {
[key: string]: typeof BaseClass; // more like constructors
};
But that is also not what Children stores:
const Children: IChildrenObj = {
C1: Child1, // error!
// Type 'typeof Child1' is not assignable to type 'typeof BaseClass'.
// Construct signature return types 'Child1' and 'BaseClass<T>' are incompatible.
C2: Child2, // error!
// Type 'typeof Child2' is not assignable to type 'typeof BaseClass'.
// Construct signature return types 'Child2' and 'BaseClass<T>' are incompatible.
}
The type typeof BaseClass has an abstract construct signature that looks something like new <T>() => BaseClass<T>; the callers (or more usefully, the subclasses that extend BaseClass) can choose anything they want for T, and BaseClass must be able to handle that. But the types typeof Child1 and typeof Child2 are not able to produce BaseClass<T> for any T that the caller of new Child1() or the extender class Grandchild2 extends Child2 wants; Child1 can only construct a BaseClass<Item1> and Child2 can only construct a BaseClass<Item2>.
So currently IChildrenObj says it holds constructors that can each produce a BaseClass<T> for every possible type T. Really what you'd like is for IChildrenObj to say it holds constructors that can each produce a BaseClass<T> for some possible type T. That difference between "every" and "some" has to do with the difference between how the type parameter T is quantified; TypeScript (and most other languages with generics) only directly supports "every", or universal quantification. Unfortunately there is no direct support for "some", or existential quantification. See microsoft/TypeScript#14446 for the open feature request.
There are ways to accurately encode existential types in TypeScript, but these are probably a little too annoying to use unless you really care about type safety. (But I can elaborate if this is needed)
Instead, my suggestion here is probably to value productivity over full type safety and just use the intentionally loose any type to represent the T you don't care about.
So, here's one way to define IChildrenObj:
type SubclassOfBaseClass =
(new () => BaseClass<any>) & // a concrete constructor of BaseClass<any>
{ [K in keyof typeof BaseClass]: typeof BaseClass[K] } // the statics without the abstract ctor
/* type SubclassOfBaseClass = (new () => BaseClass<any>) & {
prototype: BaseClass<any>;
getName: () => string;
} */
type IChildrenObj = {
[key: string]: SubclassofBaseClass
}
The type SubclassOfBaseClass is the intersection of: a concrete construct signature that produces BaseClass<any> instances; and a mapped type which grabs all the static members from typeof BaseClass without also grabbing the offending abstract construct signature.
Let's make sure it works:
const Children: IChildrenObj = {
C1: Child1,
C2: Child2,
} // okay
const nums = Object.values(Children)
.map(ctor => new ctor().itemArray.length); // number[]
console.log(nums); // [0, 0]
const names = Object.values(Children)
.map(ctor => ctor.getName()) // string[]
console.log(names); // ["Child1", "Child2"]
Looks good.
The caveat here is that, while IChildrenObj will work, it's too fuzzy of a type to keep track of things you might care about, such as the particular key/value pairs of Children, and especially the weird "anything goes" behavior of index signatures and the any in BaseClass<any>:
// index signatures pretend every key exists:
try {
new Children.C4Explosives() // compiles okay, but
} catch (err) {
console.log(err); // 💥 RUNTIME: Children.C4Explosives is not a constructor
}
// BaseClass<any> means you no longer care about what T is:
new Children.C1().itemArray.push("Hey, this isn't an Item1") // no error anywhere
So my suggestion in cases like this is to only make sure that Children is assignable to IChildrenObj without actually annotating it as such. For example, you can use a helper function:
const asChildrenObj = <T extends IChildrenObj>(t: T) => t;
const Children = asChildrenObj({
C1: Child1,
C2: Child2,
}); // okay
Now Children can still be used anywhere you need an IChildrenObj, but it still remembers all of the specific key/value mappings, and thus emits errors when you do bad things:
new Children.C4Explosives() // compiler error!
//Property 'C4Explosives' does not exist on type '{ C1: typeof Child1; C2: typeof Child2; }'
new Children.C1().itemArray.push("Hey, this isn't an Item1") // compiler error!
// Argument of type 'string' is not assignable to parameter of type 'Item1'
You can still use IChildrenObj if you need to:
const anotherCopy: IChildrenObj = {};
(Object.keys(Children) as Array<keyof typeof Children>)
.forEach(k => anotherCopy[k] = Children[k]);
Playground link to code

Assigning an id to a generic Type where the id property name is configurable?

I have a generic type E. It is an Entity instance like Customer, Product, Todo, etc. Is has two configurable fields id and guid, however they are not necessarily named that. The names for those two properties are configurable.
Here's an example of an Entity:
class Todo {
id: string;
gid: string;
title: string;
completed: boolean;
}
In order to configure the property names a config class like this is used:
/**
* Store configuration.
*/
export class StoreConfig {
idKey?:string ='id';
guidKey?:string ='gid';
}
The StoreConfig is used to determine the names of the properties on instances of type E that generated IDs are assigned to. So we have a method like this:
/**
* Post (Add a new) element to the store.
* #param e
*/
post(e: E) {
const guid: string = v1();
e[this.config.guid] = guid;
this.entries[guid] = e;
Typescript throws:
[ts] Element implicitly has an 'any' type because type '{}' has no index signature
(property) EStore.config: StoreConfig
Is there a way to fix this? It's a little trick, because the E type has two properties wholes names are configurable ...?
Also the class essentially has two index signatures. One for the id property (Also not yet named / it is configurable) and one for the guid property (Also not yet named / it is configurable) ...
If I do:
(<any>e)[this.config.guidKey] = guid;
Typescript does not complain, but I'm wondering if there's a more elegant way to declare this?
You'll need type parameters for the names of the two properties. (Well, since the properties have the same type string, you could use a single type parameter with a union type, but that's a hack IMO.) Then you can use a mapped type to declare the actual properties.
interface StoreConfig<I extends string, G extends string> {
idKey: I;
guidKey: G;
}
type EntityBase<I extends string, G extends string> =
// TODO: In TypeScript 3.1, change to the following for elegance:
//{[P in I]: string} & {[P in G]: string}
{[P in I | G]: string};
class Store<I extends string, G extends string, E extends EntityBase<I, G>> {
constructor(private config: StoreConfig<I, G>) {}
entries: {[guid: string]: E};
post(e: E) {
const guid: string = v1();
e[this.config.guidKey] = guid;
this.entries[guid] = e;
}
}
// https://github.com/Microsoft/TypeScript/issues/10571 will make this nicer.
new Store<"id", "gid", Todo>({idKey: "id", guidKey: "gid"});

How to reduce javascript object to only contain properties from interface

When using typescript a declared interface could look like this:
interface MyInterface {
test: string;
}
And an implementation with extra property could be like this:
class MyTest implements MyInterface {
test: string;
newTest: string;
}
Example (here the variable 'reduced' still contain the property 'newTest'):
var test: MyTest = {test: "hello", newTest: "world"}
var reduced: MyInterface = test; // something clever is needed
Question
In a general way, how can you make the 'reduced' variable to only contain the properties declared in the 'MyInterface' interface.
Why
The problem occur when trying to use the 'reduced' variable with angular.toJson before sending it to a rest service - the toJson method transforms the newTest variable, even if it's not accessible on the instance during compile, and this makes the rest service not accept the json since it has properties that shouldn't be there.
It is not possible to do this. The reason being interface is a Typescript construct and the transpiled JS code is empty
//this code transpiles to empty!
interface MyInterface {
test: string;
}
Thus at runtime there is nothing to 'work with' - no properties exist to interrogate.
The answer by #jamesmoey explains a workaround to achieve the desired outcome.
A similar solution I use is simply to define the 'interface' as a class -
class MyInterface {
test: string = undefined;
}
Then you can use lodash to pick the properties from the 'interface' to inject into you object:
import _ from 'lodash'; //npm i lodash
const before = { test: "hello", newTest: "world"};
let reduced = new MyInterface();
_.assign(reduced , _.pick(before, _.keys(reduced)));
console.log('reduced', reduced)//contains only 'test' property
see JSFiddle
This is a pragmatic solution that has served me well without getting bogged down on semantics about whether it actually is an interface and/or naming conventions (e.g. IMyInterface or MyInterface) and allows you to mock and unit test
TS 2.1 has Object Spread and Rest, so it is possible now:
var my: MyTest = {test: "hello", newTest: "world"}
var { test, ...reduced } = my;
After that reduced will contain all properties except of "test".
Another possible approach:
As other answers have mentioned, you can't avoid doing something at runtime; TypeScript compiles to JavaScript, mostly by simply removing interface/type definitions, annotations, and assertions. The type system is erased, and your MyInterface is nowhere to be found in the runtime code that needs it.
So, you will need something like an array of keys you want to keep in your reduced object:
const myTestKeys = ["test"] as const;
By itself this is fragile, since if MyInterface is modified, your code might not notice. One possible way to make your code notice is to set up some type alias definitions that will cause a compiler error if myTestKeys doesn't match up with keyof MyInterface:
// the following line will error if myTestKeys has entries not in keyof MyInterface:
type ExtraTestKeysWarning<T extends never =
Exclude<typeof myTestKeys[number], keyof MyInterface>> = void;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type 'UNION_OF_EXTRA_KEY_NAMES_HERE' does not satisfy the constraint 'never'
// the following line will error if myTestKeys is missing entries from keyof MyInterface:
type MissingTestKeysWarning<T extends never =
Exclude<keyof MyInterface, typeof myTestKeys[number]>> = void;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type 'UNION_OF_MISSING_KEY_NAMES_HERE' does not satisfy the constraint 'never'
That's not very pretty, but if you change MyInterface, one or both of the above lines will give an error that hopefully is expressive enough that the developer can modify myTestKeys.
There are ways to make this more general, or possibly less intrusive, but almost no matter what you do, the best you can reasonably expect from TypeScript is that your code will give compiler warnings in the face of unexpected changes to an interface; not that your code will actually do different things at runtime.
Once you have the keys you care about you can write a pick() function that pulls just those properties out of an object:
function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K> {
return keys.reduce((o, k) => (o[k] = obj[k], o), {} as Pick<T, K>);
}
And them we can use it on your test object to get reduced:
var test: MyTest = { test: "hello", newTest: "world" }
const reduced: MyInterface = pick(test, ...myTestKeys);
console.log(JSON.stringify(reduced)); // {"test": "hello"}
That works!
Playground link to code
Are you trying to only set/assign properties listed on the interface only? Functionality like that is not available in TypeScript but it is very simple to write a function to perform the behaviour you looking for.
interface IPerson {
name: string;
}
class Person implements IPerson {
name: string = '';
}
class Staff implements IPerson {
name: string = '';
position: string = '';
}
var jimStaff: Staff = {
name: 'Jim',
position: 'Programmer'
};
var jim: Person = new Person();
limitedAssign(jimStaff, jim);
console.log(jim);
function limitedAssign<T,S>(source: T, destination: S): void {
for (var prop in destination) {
if (source[prop] && destination.hasOwnProperty(prop)) {
destination[prop] = source[prop];
}
}
}
In your example newTest property won't be accessible thru the reduced variable, so that's the goal of using types. The typescript brings type checking, but it doesn't manipulates the object properties.
In a general way, how can you make the 'reduced' variable to only contain the properties declared in the 'MyInterface' interface.
Since TypeScript is structural this means that anything that contains the relevant information is Type Compatible and therefore assignable.
That said, TypeScript 1.6 will get a concept called freshness. This will make it easier to catch clear typos (note freshness only applies to object literals):
// ERROR : `newText` does not exist on `MyInterface`
var reduced: MyInterface = {test: "hello", newTest: "world"};
Easy example:
let all_animals = { cat: 'bob', dog: 'puka', fish: 'blup' };
const { cat, ...another_animals } = all_animals;
console.log(cat); // bob
One solution could be to use a class instead of an interface and use a factory method (a public static member function that returns a new object of it's type). The model is the only place where you know the allowed properties and it's the place where you don't forget to update them accidentaly on model changes.
class MyClass {
test: string;
public static from(myClass: MyClass): MyClass {
return {test: myClass.test};
}
}
Example:
class MyTest extends MyClass {
test: string;
newTest: string;
}
const myTest: MyTest = {test: 'foo', newTest: 'bar'};
const myClass: MyClass = MyClass.from(myTest);

Categories