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 am trying to create a class or constructor function, which I specify a list of values to be used in the type of one of it's methods.
As an example I have this code.
const MyAnimal = function (animal: string[]) {
type Animal = typeof animal[number]
this.getAnimal = (url: Animal) => {
console.log(url)
}
}
const animalTest = new MyAnimal(['sheep', 'dog', 'cat'])
// I would like this to fail as 'mouse' is not part of the array ['sheep', 'dog', 'cat']
animalTest.getAnimal('mouse')
I want getAnimal to have the type 'sheep' | 'dog' | 'cat' and for the intellisense to warn me if I add something different
Is this possible?
You can do that via a generic type parameter and a readonly array of animals, like this;
class MyAnimal<Animal> {
constructor(public animals: readonly Animal[]) {
}
getAnimal(url: Animal) {
console.log(url);
}
}
const animalTest = new MyAnimal(['sheep', 'dog', 'cat'] as const);
animalTest.getAnimal('mouse'); // Error as desired
animalTest.getAnimal('sheep'); // Works
Playground link
TypeScript can infer the string literal union type to provide as the type argument for the Animal type parameter because the array is readonly (which we satisfy when calling the constructor via as const), so TypeScript knows it won't change at runtime.
In order to infer literal type, you can use variadic tuple types:
type Animal<Name extends string> = {
name: Name,
url: `https://${string}`
}
class MyAnimal<
Name extends string,
Item extends Animal<Name>,
List extends Item[]
> {
constructor(public animals: [...List]) {
}
// You should get "url" from infered list and not from Item["url"]
getAnimal<Url extends List[number]['url']>(url: Url) {
console.log(url);
}
}
const animalTest = new MyAnimal(
[
{ name: 'sheep', url: 'https://sheep.com', },
{ name: 'dog', url: 'https://dog.com', },
{ name: 'cat', url: 'https://cat.com', }
]);
animalTest.getAnimal('https://dog.com'); // ok
animalTest.getAnimal('https://mouse.com'); // expected error
Playground
If you want to learn more about literal type inference, you can check my article. The goal is to provide as much as possible constraints to generic parameter. If there are enough constraints TS will infer literal type.
UPDATED VERSION:
I have several implementations of a generic class, which is wrapped in the vue3 composition api function "reactive", which is basically a proxy which keeps track of any updates:
export declare function reactive<T extends object>(target: T): UnwrapNestedRefs<T>;
How can I implement a class so that the constructor takes an object of such implementations, and have such inner types unwrapped as parameter of a callback function which is the second argument of the constructor?
Like this:
const campaignWrapped = reactive(new MyWrapper<Campaign>(campaign));
const batchesWrapped = reactive(new MyWrapper<Batch[]>(batches));
new MyClass({ first: campaignWrapped, second: batchesWrapped }, (states) => {
states.first; // Should be of type Campaign
states.second; // Should be of type Batch[]
});
It seems like it's not possible to derive the type from the proxy object, because extends MyClass<infer U> ? ... doesn't match anymore. The only way around seems to be adding an interface to the Class and check for this interface instead. Even if I unwrap UnwrapNestedRefs first and infer it's type, I still can't get the real inner type.
OLD VERSION: (simplified)
(just for the context, as my wrong attempt might help someone to understand the difference between union and intersect - which wasn't clear to me)
I want to get the unboxed types in a callback, which have to be derived from generic types.
So far so good. It seems to work if I have only one record entry.
But as soon as there is another, it fails.
The resulting type looks ALMOST correct:
Record<"first", Campaign> | Record<"second", Batch[]>
How can I tell typescript to use intersect and not union? So the type would become:
Record<"first", Campaign> & Record<"second", Batch[]>
(BONUS QUESTION: Is it possible to get rid of the LSW interface and use the type of the class directly? For me the extends LazyStateWrapper<infer U> ? ... didn't work.)
Working example in typescriptlang.org playground here
And here is the code inline:
type UnwrapInner<X, S extends string> = X extends LSW<infer X>
? Record<S, X>
: never;
type UnwrapLazyStateWrapper<T> = T extends Record<infer S, infer U> ? (S extends string ? UnwrapInner<T[S], S> : never) : never;
export class TransientLazyState<X, T extends object> {
constructor(states: X, cb: (states: UnwrapLazyStateWrapper<X>) => T | undefined) {
console.log('todo');
}
}
interface LSW<T extends object> {
getStateClone(): T | undefined;
}
export class LazyStateWrapper<T extends object> implements LSW<T> {
private obj: T;
constructor(instance: T) {
this.obj = instance;
}
getStateClone(): T | undefined {
return {} as T;
}
}
type Campaign = { id: number; foo: string };
type Batch = { id: number; bar: string };
const campaign: Campaign = { id: 123, foo: 'x' };
const batches: Batch[] = [];
const campaignWrapped = new LazyStateWrapper(campaign);
const batchesWrapped = new LazyStateWrapper(batches);
const test1 = new TransientLazyState({ first: campaignWrapped }, (states) => {
const xy = states.first; // Yay. Works! Is of type "Campaign"
console.log(xy);
return undefined;
});
const test2 = new TransientLazyState({ first: campaignWrapped, second: batchesWrapped }, (states) => {
const xy = states.first; // Doesn't work anymore, once there are more records. But to me the type looks ALMOST correct. ERROR: Property 'first' does not exist on type 'Record<"first", Campaign> | Record<"second", Batch[]>'. Property 'first' does not exist on type 'Record<"second", Batch[]>'.(2339)
console.log(xy);
return undefined;
});
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'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);