I created this HashMap interface:
export interface HashMap<K, V> {
[name: K]: V;
}
With the idea that I can use it as:
const map: HashMap<String, String>;
However, right now I get an error saying name can only be of type string or number.
How do I apply the constraint on K template here?
Firstly, do not use the UpperCase primitive type names. That is to say use string instead of String.
As to the meat of your question, a TypeScript index signature's parameter type must be one of string, number, or symbol. The last is only available under --target es2015 or greater.
You can, however, constraint the type of the key to be a subset of string values. In this case you must use a type alias declaration, not an interface.
export type HashMap<K extends string, V> = {
[P in K]?: V;
}
The [P in K] syntax means "enumerate each string literal type in the subset of string types with which K is instantiated".
This is useful because we can restrict the contents of the map by specifying a union type with string literal types as its constituents.
For example:
const map: HashMap<'firstName' | 'lastName', string> = {
firstName: 'John', // OK
nickname: 'Johnny' // Error
};
Basically, for all intents and purposes you must use string but you can constrain the key type to be a specific string or a set of specific strings by using a union type.
In practice, the string union type will very often be a function of another type.
For example:
interface Item {
name: string;
id: number;
}
interface PropertyMetadata {
kind: 'data' | 'accessor';
}
type LazyItem = {
[P in keyof Item]: PropertyDescriptor
};
keyof is a Type Operator that takes a type and returns a type that is the string union of its property keys.
Now, this is probably not what you are looking for. If you want to use an arbitrary key type, bound by some constraint you will need to use an ES2015 Map object. Prior to the addition of this type in JavaScript, it was not possible to perform this mapping in a clean way and string was essentially the only viable key type.
By combining an ES2015 map with the power of TypeScript generics (templates as you call them) we can approximate what you are looking for.
For example:
interface Category {
name: string;
products: Product[];
}
interface Product {
name: string;
category: Category;
}
const categoriesToProducts = new Map<Category, Product[]>();
declare function getProducts(): Product[];
const products = getProducts();
products.forEach(product => {
const mapped = categoriesToProducts.get(product.category);
if (mapped) {
mapped.push(product);
}
else {
categoriesToProducts.add(product.category, [product]);
}
});
Since the only types you can normally use as keys for indexable types are primitive types string and number, I don't think there is much gained from the ability to parametrize on them, but if you insist...
type IndexableTypes<V> = {
[s: string]: { [n: string]: V };
number: { [n: number]: V };
}
type SN = { [n: number]: 'number', [s: string]: string };
type HashMap<K extends string | number, V> = IndexableTypes<V>[SN[K]];
const map: HashMap<string, string> = {}; // inferred as {[n: string]: string; }
const map1: HashMap<number, string> = {}; // inferred as {[n: number]: string;}
const map2: HashMap<number, HashMap<string, number>> = {};
// inferred as
{[n: number]:
{[n: string]: number;};
}
const map4: HashMap<boolean, {}> = {};
// error: Type 'boolean' does not satisfy the constraint 'string | number'.
However, you could just use es2015 Map class if it's available in your runtime:
type MyMap<K extends string | number, V> = Map<K, V>
Related
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
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"];
}
In summary, is it possible to have an interface that declares some base properties, but does not restrict additional properties? This is my current situation:
I'm using the Flux pattern, which defines a generic dispatcher:
class Dispatcher<TPayload> {
dispatch(arg:TPayload):void { }
}
I then create a dispatcher with my own payload type, like this:
interface ActionPayload {
actionType: string
}
const dispatcher = new Dispatcher<ActionPayload>();
Now I have some action code that should dispatch a payload with some additional data, but the ActionPayload interface only allows for actionType. In other words, this code:
interface SomePayload extends ActionPayload {
someOtherData: any
}
class SomeActions {
doSomething():void {
dispatcher.dispatch({
actionType: "hello",
someOtherData: {}
})
}
}
Gives a compile-error because someOtherData does not match the ActionPayload interface. The issue is that many different "action" classes will re-use the same dispatcher, so while it's someOtherData here it might be anotherKindOfData over there, and so on. At the moment, all I can do to accomodate this is use new Dispatcher<any>() because different actions will be dispatched. All actions share a base ActionPayload, though, so I was hoping to be able to constrain the type like new Dispatcher<extends ActionPayload>() or something. Is something like that possible?
If you want ActionPayload to accept any other property you can add an indexer:
interface ActionPayload {
actionType: string;
// Keys can be strings, numbers, or symbols.
// If you know it to be strings only, you can also restrict it to that.
// For the value you can use any or unknown,
// with unknown being the more defensive approach.
[x: string | number | symbol]: unknown;
}
See https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#strict-object-literal-assignment-checking
interface Options {
darkMode?: boolean;
[otherOptions: string]: unknown;
}
I solved this by creating something like
type Make = "Volvo" | "Polestar" | "Saab";
interface BaseCar {
make: Make;
horsePower: number;
allWheelDrive: boolean;
}
type Car = BaseCar & Record<string, string|number|boolean>;
If your type is inferred from the object definition, you can assert the type as the union of the original type and {[key: string]: any]}:
const obj = { foo: 42 };
return obj as typeof obj & {[key: string]: any]};
In this way, the IntelliSense will suggest you foo, but won't complain when you try to assign or retrieve another key.
Anyway, if the additional keys are few and they're known a priori, you can just add them as optional in the type definition:
const obj: {foo: number; bar?: string} = {foo: 42}
With TypeScript 3.9+ and strict set to true in tsconfig.json, I get errors with the accepted answer.
Here is the solution:
interface ActionPayload {
actionType: string;
// choose one or both depending on your use case
// also, you can use `unknown` as property type, as TypeScript promoted type,
// but it will generate errors if you iterate over it
[x: string]: any;
[x: number]: any;
}
This avoids errors like:
An index signature parameter type must be either 'string' or 'number'.
symbol is not allowed
An index signature parameter type cannot be a union type. Consider using a mapped object type instead.
string | number is not allowed in [x: string | number]: unknown;
I think I found what I was looking for. I can cast the dispatched object to SomePayload, and TSC validates that its compatible with both the cast interface and the TPayload of the dispatcher:
dispatcher.dispatch(<SomePayload>{
actionType: "hello",
someOtherData: {}
})
Example online.
This might a be relatively noob question,
I have an interface
interface Employee {
name: string
}
and I would like to have an extended version of this after it being saved into the DB:
interface EmployeeDb {
id: string,
name: string
}
I would like to differentiate it when handling checks so after saving data in my storage, the type checker won't complain about not having id value. Meaning I want to avoid using this:
interface Employee {
id?: string,
name: string
}
so I don't have to check for id everywhere.
So I am trying to do it this way:
type Employee = {
name: string
}
type IDatabaseObject<T> = {
id: IDatabaseObjectId;
[P in keyof T]: T[P];
};
type EmployeeDb = IDatabaseObject<Employee>
which the IDE gives an error with the top syntax
A computed property name must be of type 'string', 'number', 'symbol',
or 'any'.ts(2464)
so I tried to use interface and extend it
interface IDatabaseObject {
id: string
}
interface EmployeeDb extends Employee, IDatabaseObject {}
but in the backend code when I try to use this setup I get an error from vscode eslint again. I have a small code here that adds the data to localstorage, generates a id and returns the data. see code:
class DbAsyncStorageTemplate<
InputDataType,
OutputDataType extends IDatabaseObject
> {
async addEntry(object: InputDataType): Promise<OutputDataType> {
const id: string = generateUuid()
const dbObject = { id, ...object }
dbObject.id = id
// add the item to AsyncStorage directly
await AsyncStorage.setItem(id, JSON.stringify(object))
// ERROR HERE: return the new object
return dbObject as OutputDataType
}
}
}
but I get an error from the IDE (eslint) for the last line
Conversion of type '{ id: string; } & InputDataType' to type
'OutputDataType' may be a mistake because neither type sufficiently
overlaps with the other. If this was intentional, convert the
expression to 'unknown' first. '{ id: string; } & InputDataType' is
assignable to the constraint of type 'OutputDataType', but
'OutputDataType' could be instantiated with a different subtype of
constraint 'any'.
any recommendation on how to do this properly?
I believe you're looking for intersections of types.
type Employee = {
name: string
}
type EmployeeDb = {
id: string;
} & Employee;
You could also define the raw DB interface and use Pick or Omit utilities as needed.
Pick Utility
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Pick<Todo, "title" | "completed">;
const todo: TodoPreview = {
title: "Clean room",
completed: false,
};
I think you are looking for this: https://www.typescriptlang.org/docs/handbook/advanced-types.html#:~:text=an%20intersection%20type%3A-,//%20Use%20this%3A,%7D,-Try
You are trying to create a new type (IDatabaseObject) based on an old type (Employee, for instance; or T, in the generic case). This is a Mapped Type.
In your code,
[P in keyof T]: T[P]
returns your old type rather than members of that old type. So you need to close the curly brackets and intersect it with any other new members you want to add.
i.e. do the following for IDatabseObject
type IDatabaseObject<T> = {
id: number;
} & {
[P in keyof T]: T[P];
};
I have an object containing multiple types of values:
interface Foo {
id: number;
data: FooData;
username: string;
notes: string;
}
const foo: Foo = {
...
}
I have a function that requires a string, and am iterating through a specific list of fields in my object to use in that function, all of which contain string values:
const renderString = (value: string) => {
...
}
const fooKeys: keyof Foo = ["username", "notes"];
fooKeys.map((key) => {
renderString(foo[key])
}
The issue is that foo[key] can be a string, number, or FooData object, so I want to specify that foo[key] will ONLY be a field value of Foo with a key matching one in fooKeys, since they are all going to be string values.
We can assert foo[key] as string but that doesn't protect us against bad keys in fooKeys.
One option is to use a conditional type to only allow string properties since that appears to be the only ones allowed? For example:
type StringPropsOnly<T> = {
[Key in keyof T]: T[Key] extends string ? Key : never;
}[keyof T]
const fooKeys: StringPropsOnly<Foo>[] = ["username", "notes"];
fooKeys.map((key) => {
renderString(foo[key])
});
TypeScript Playground
This answer I found mentions as const:
const fooKeys = ["username", "notes"] as const;